前言
使用多进程不一定非要用来并行加速,也可以方便地运行多个实验,这里对多进程、多线程的一些技巧和知识做一下记录。
正文
首先是一些知识的介绍。
apply是阻塞式的。首先主进程开始运行,碰到子进程,操作系统切换到子进程,等待子进程运行结束后,在切换到另外一个子进程,直到所有子进程运行完毕。然后在切换到主进程,运行剩余的部分。相当于在单进程中串行执行的。
apply_async是异步非阻塞式的。他不会等待子进程执行完毕, 主进程会继续执行, 根据系统调度来进行进程切换,真正的并行。
并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。
多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响;而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改。因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了,也就有了锁的诞生,确保原子性,即事务是一个不可再分割的工作单元,事务中的操作要么都发生,要么都不发生。多进程也可以设置一些共享的变量。
Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
我们通过一个代码来介绍这些技巧,主要是两种方式,一种直接map传参,相当于对输入对象中的每个元素进行多进程运算;另一种是一个个进程异步地启动。
from multiprocessing import Pool
# 需要并行的函数
def f(x):
return x*x
def cmp_string(s1):
print(s1 is 'abc')
if __name__ == '__main__':
'''
设置进程池,它提供了一种快捷的方法,赋予函数并行化处理一系列输入值的能力,
可以将输入数据分配给不同进程处理(数据并行)。
不设置默认根据本地的cpu个数决定,processes小于等于本地的cpu个数;
如果池中的进程数已经达到规定最大值,那么该请求就会等待,
直到池中有进程结束,才会创建新的进程来执行它。
'''
with Pool(5) as p:
print(p.map(f, [1, 2, 3])) # 传参
'''
输出为:[1, 4, 9]
'''
p = Pool(12) # 设置进程池,最多并行12个
x = 'abc'
for i in range(2):
# 异步地启动一个进程,参数为x。这里逗号不能省略,否则会解析为3个参数
res = p.apply_async(cmp_string, args=(x,))
# 获取多进程的错误信息,否则报错了也不会显示。可以试试把上面的逗号去掉,看错误信息
res.get()
print('子进程开始...')
# 关闭进程池,不会有新的进程加入到pool
p.close()
# 主进程本身很短,join函数等待所有子进程结束之后主进程才继续运行
p.join()
print('所有子进程结束!')
这里可以看出cmp_string的结果是False,如果不清楚判断string的方式,可以看这篇文章。