Python并行计算专题
最近工作中经常涉及到Python并行计算的内容,所以今天做一期专题的知识整理。本文将涉及以下三块内容:1. 多核/多线程/多进程的概念区分,2. Python多线程,多进程的使用方式,3. Python进程池的管理方案
多核/多线程/多进程
一般而言,计算机可以并行执行的线程数量就是CPU的物理核数,因为CPU只能看到线程(线程是CPU调度分配的最小单位)。然而,由于超线程技术的存在,现在的CPU可以在每个内核上运行更多的线程,因此,现代计算机可以并行执行的线程数量往往是CPU物理核数的两倍或更多
进程是操作系统资源分配(内存,显卡,磁盘等)的最小单位,线程是执行调度(即CPU资源调度)的最小单位(CPU看到的都是线程而不是进程)。一个进程可以有一个或多个线程,线程之间共享进程的资源。如果计算机有多个内核,且计算机中的总的线程数量小于逻辑核数,那线程就可以并行运行在不同的内核中。如果是单核多线程,那多线程之间就不是并行的关系,它们只是通过分时的方式,轮流使用单个内核的计算资源,营造出一种“并发”执行的假象。由于线程切换需要耗费额外的资源,因此如果多线程协同处理的是同一个计算任务,那么该任务的完成速度是不如单线程从头算到尾的
针对计算密集型的任务,我们使用与逻辑内核数相同的线程数就可以最大化地利用计算资源(线程数少了会引起内核资源的闲置,线程数多了则会消耗不必要的资源在分时切换上)。如果是IO密集型任务,我们就需要创建更多的线程,因为当一个线程已经算好结果,在等待IO写入时,我们就可以让另一个线程去使用此时空闲的内核资源,在这个场景下线程间切换的代价是小于内核资源闲置的代价的
在上一段中,我们讨论的一直都是线程,那么什么时候我们应该使用更多的进程呢?回顾之前提到过的进程的特点:
进程是操作系统资源分配的最小单位
因此,是否使用多进程,这取决于你是否想要,并且是否能够发挥某一系统资源IO性能的最大值。举个例子:比如你想要尽可能快地往磁盘中写入数据,而此时一个进程并不能占满磁盘IO的带宽,那么我们就可以使用多进程,并发地往磁盘中写入内容,进而最大化地发挥磁盘的读写性能
一般而言,只要CPU 有多个逻辑内核,那么多个线程就能够在不同的内核上并发执行(即使此时只有一个进程)。但对 Python 来说,无论是单核还是多核,一个进程同时只能有一个线程在执行。为什么会出现这种情况呢?因为Python 在设计时采用了一种叫 GIL 的机制。GIL 的全称为 Global Interpreter Lock (全局解释器锁),它出于安全考虑,限制了每个Python进程下的线程,使其只有拿到GIL后,才能使用内核资源继续工作。因此,Python的多线程一般只用来实现分时的“并发”,要想充分发挥硬件性能,还是需要使用多个进程
Python多线程和多进程的使用方法
多线程
我们先来看Python多线程的一种最简单的使用方式:
import time
from threading import Thread
def foo(content, times):
for i in range(times):
timestamp = time.strftime('%H:%M:%S',time.localtime(time.time()))
print(f"{content} {i+1} {timestamp}")
time.sleep(1)
thread_names = ('alpha', 'bata')
repeat_times = 3
for name in thread_names:
th = Thread(target=foo, args=(name, repeat_times))
th.start()
time.sleep(0.1)
输出:
alpha 1 21:57:58
bata 1 21:57:58
alpha 2 21:57:59
bata 2 21:57:59
alpha 3 21:58:00
bata 3 21:58:00
Python通过标准库threading提供对线程的支持,其常用方法包括:
threading.currentThread()
——返回当前的线程变量threading.enumerate()
——返回一个包含正在运行的线程的list
以及Thread对象的常用方法:
start()
:启动线程活动join()
:阻塞当前的程序,直至等待时间结束或其Thread对象运行终止。该方法可用于计量多线程程序用时isAlive()
:返回线程是否活动的getName()
返回线程名,setName()
设置线程名
当然,Python中的Thread不仅仅可以通过函数的方式使用,还可以以面向对象的方式使用,参考文档说明:
The
Thread
class represents an activity that is run in a separate thread of control. There are two ways to specify the activity: by passing a callable object to the constructor, or by overriding therun()
method in a subclass. No other methods (except for the constructor) should be overridden in a subclass. In other words, only override the__init__()
andrun()
methods of this class.
当我们以面向对象的方式使用Thread时,我们只需要把需要并行的部分实现至类的run()
方法中,然后在外部调用线程对象的start()
方法即可:
import time
import threading
class MyThread(threading.Thread):
def __init__(self, name, repeat_times):
super(MyThread, self).__init__()
self.content = name
self.times = repeat_times
def run(self):
for i in range(self.times):
timestamp = time.strftime('%H:%M:%S',time.localtime(time.time()))
print(f"{self.content} {i+1} {timestamp}")
time.sleep(1)
thread_names = ('alpha', 'bata')
repeat_times = 3
for name in thread_names:
th = MyThread(name, repeat_times)
th.start()
time.sleep(0.1)
该程序的输出与之前的多线程函数一致
多进程
Python多进程可以使用subprocess模块,参考:multiprocessing — Process-based parallelism - Python Docs
from multiprocessing import Process
import os
# 子进程要执行的代码
def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))
if __name__=='__main__':
print('Parent process %s.' % os.getpid())
p = Process(target=run_proc, args=('test',))
print('Child process will start.')
p.start()
p.join()
print('Child process end.')
进程池
Python进程池的使用可参考:Python使用进程池管理进程
from multiprocessing import Pool
import time
import os
def action(name='http://c.biancheng.net'):
print(name,' --当前进程:',os.getpid())
time.sleep(3)
if __name__ == '__main__':
#创建包含 4 条进程的进程池
pool = Pool(processes=4)
# 将action分3次提交给进程池
pool.apply_async(action)
pool.apply_async(action, args=('http://c.biancheng.net/python/', ))
pool.apply_async(action, args=('http://c.biancheng.net/java/', ))
pool.apply_async(action, kwds={'name': 'http://c.biancheng.net/shell/'})
pool.close()
pool.join()
参考: