并发编程是刚需,尤其是在多I/O操作时,多线程,协程,多进程三路英雄各显神通。多线程,协程属于并发操作,多进程属于并行操作,那么你是否清楚了什么是并发,什么是并行?
并发与并行的区别
借用知乎用户的回答:
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。
多线程: 在 Python 里,由于有全局锁 (GIL) 的存在,并发就是多个线程轮流使用 CPU,同一时刻只一个线程在工作,操作系统会在合适的时间进行切换,由于线程的切换速度非常快,给人的感觉是多个任务都在运行。在 I/O 密集型任务场景中,线程切换后,I/O 操作仍然在进行,线程 1 在进行 I/O 操作时,线程 2 可以获得 CPU 资源进行计算,虽然增加了切换成本,却提高了效率。
协程:
协程是轻量级线程,是单线程,却可以执行并发任务,原因是协程把切换的权利交给程序员,与程序员决定在哪些环节进行切换。协程可以处理上万的并发,多线程即不可以,因为切换成本太大,会耗尽计算机资源,可以搜索下 C10K 问题。
多进程: 并行,真正的同一时刻多个任务同时进行。如果想使用多核,就选多进程。
Python 协程标准库只有一个,即 asyncio,而支持多线程,多进程的标准库却有两个:Concurrent.futures 和 Multiprocessing。本文分享一下这两者的使用区别。先看下基本用法。
Multiprocessing
Multiprocessing 即有线程池,也是进程池,简单的使用方法如下:
线程池:
from multiprocessing.dummy import Pool as ThreadPool
with ThreadPool(processes=100) as executor:
executor.map(func, iterable)
进程池:
from multiprocessing import Pool as ProcessPool
with ProcessPool(processes=10) as executor:
executor.map(func, iterable)
Concurrent.futures
线程池:
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=5) as executor:
executor.map(function, iterable)
进程池:
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor(max_workers=5) as executor:
executor.map(function, iterable)
有没有觉得他们的使用方法简直一模一样,那么官方为何要提供这样两个标准库呢?
二者区别
其实,本质区别并不大,有的也只是调用方式略有差异。
先有的 multiprocessing,后有的 concurrent.futures,后者的出现就是为了降低编写代码的难度,后者的学习成本较低。
在速度上,并无谁快谁慢之说。获得多少加速(如果有)取决于硬件,操作系统的详细信息,尤其取决于特定任务需要多少进程间通信。在后台,所有进程都依赖于相同的 OS 原语,使用这些原语的高级 API 并不是j影响速度的主要因素。接下来分享下二者的详细用法。
关于 concurrent.futures
官方说 concurrent.futures 模块是更高级别的接口,主要是因为它让程序员并发和并行的代码更简单了。该模块提供以下对象和函数:
- 期程对象:concurrent.futures.Future
- 模块函数:concurrent.futures.wait
- 执行器对象:concurrent.futures.{Executor,ThreadPoolExecutor,ProcessPoolExecutor}
比如,Futures 中的 Executor 类,当我们执行 executor.submit(func) 时,它便会安排里面的 func() 函数执行,并返回创建好的 future 实例,以便你之后查询调用。
这里再介绍一些常用的函数。Futures 中的方法 done(),表示相对应的操作是否完成——True 表示完成,False 表示没有完成。不过,要注意,done() 是 non-blocking 的,会立即返回结果。相对应的 add_done_callback(fn),则表示 Futures 完成后,相对应的参数函数 fn,会被通知并执行调用。
Futures 中还有一个重要的函数 result(),它表示当 future 完成后,返回其对应的结果或异常。而 as_completed(fs),则是针对给定的 future 迭代器 fs,在其完成后,返回完成后的迭代器。
官方给的 ThreadPoolExecutor 例子:
import concurrent.futures
import urllib.request
URLS = ['http://www.foxnews.com/',
'http://www.cnn.com/',
'http://europe.wsj.com/',
'http://www.bbc.co.uk/',
'http://some-made-up-domain.com/']
# Retrieve a single page and report the URL and contents
def load_url(url, timeout):
with urllib.request.urlopen(url, timeout=timeout) as conn:
return conn.read()
# We can use a with statement to ensure threads are cleaned up promptly
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
# Start the load operations and mark each future with its URL
future_to_url = {executor.submit(load_url, url, 60): url for url in URLS}
for future in concurrent.futures.as_completed(future_to_url):
url = future_to_url[future]
try:
data = future.result()
except Exception as exc:
print('%r generated an exception: %s' % (url, exc))
else:
print('%r page is %d bytes' % (url, len(data)))
请注意:
ProcessPoolExecutor 是 Executor 的子类,它使用进程池来实现异步执行调,使用 multiprocessing 回避 Global Interpreter Lock 但也意味着,作为进程的函数只可以处理和返回可序列化的对象, __main__
模块必须可以被子进程导入,这意味着 ProcessPoolExecutor 不可以工作在交互式解释器中。
关于 multiprocessing
multiprocessing 是一个用于产生进程的包,具有与 threading 模块相似 API。 multiprocessing 包同时提供本地和远程并发,使用子进程代替线程,有效避免 Global Interpreter Lock 带来的影响。因此,multiprocessing 模块允许程序员充分利用机器上的多核。可运行于 Unix 和 Windows 。
multiprocessing 模块还引入了在 threading 模块中没有的 API。一个主要的例子就是 Pool 对象,它提供了一种快捷的方法,赋予函数并行化处理一系列输入值的能力,可以将输入数据分配给不同进程处理(数据并行)。下面的例子演示了在模块中定义此类函数的常见做法,以便子进程可以成功导入该模块。这个数据并行的基本例子使用了 Pool ,
from multiprocessing import Pool
def f(x):
return x*x
if __name__ == '__main__':
with Pool(5) as p:
print(p.map(f, [1, 2, 3]))
结论
因此,简单的并发应用,请使用 concurrent.futures,复杂些的,要自己动手实现的,请使用 multiprocessing 吧。初学者直接学习 concurrent.futures。
参考文档:
https://docs.python.org/zh-cn/3/library/concurrent.futures.html
https://docs.python.org/zh-cn/3/library/multiprocessing.html