本文的目的是解释为什么在Python中需要多线程和多处理,何时使用多线程和多进程,以及它们能怎样提高我们程序的性能。
假设我们的量化模型需要从多个网站爬取一些数据,我们将要对比用单线程和多线程的方法有何性能上的差别。
1,单线程,单进程
在单线程、单进程中,我们将用for循环读取一个url列表。
如您所见,我们只是使用for循环一个接一个地遍历url并读取响应。我们可以使用IPython的%%time函数对消耗的时间进行统计,这个读取13个网页的任务大约需要12秒钟。
2,多线程
现在我们改进一下这个程序,我们可以将读取url的任务分配给多个线程来完成,而不是只让一个线程去逐一读取。
比如4个线程:
8个线程:
16个线程:
用到16个线程时,这个任务的耗时已经从12.3秒缩短到了1.32秒。
使用多线程可以显著加快许多与io绑定的任务。在这里,读取url所花费的大部分时间是由于网络延迟。与io绑定的程序大部分时间都在等待输入/输出,无所事事。这可能是来自网络、数据库、文件甚至用户的I/O。这种I/O往往要花费大量的时间,因为源本身可能需要在传递I/O之前执行自己的处理。例如,CPU的工作速度比网络连接传输数据的速度快得多。
多线程可以显著提高我们爬取网页任务的效率。
3,多重处理
另一个可以提高效率的手段是多重处理。
比如我们有一个任务是计算100万以内所有质数的和。
如果只用单个进程:
如果使用多进程:
和多线程类似,多进程也是将任务(比如判断一系列数是否是质数)拆分再汇总,以此提高效率。
由于现代CPU通常有多个核心,我们可以通过使用多处理模块来加快CPU绑定任务的速度。CPU绑定任务是花费大部分时间在CPU上执行计算的程序(数学计算、图像处理等)。如果计算可以彼此独立地执行,我们就可以将它们分配到可用的CPU内核中,从而显著提高处理速度。
我们所要做的就是:
1,定义要应用的函数
2,准备要应用功能的项目列表
3,使用Pool生成进程。传递给Pool()的数字将是生成的进程数。在with语句中嵌入可以确保在完成执行后终止进程。
4,使用池进程的map函数组合输出。映射函数的输入是要应用于每个项的函数,以及项列表。
注意:可以定义该函数,以便执行任何可以并行执行的任务。例如,函数可能包含将计算结果写入文件的代码。
那么,为什么我们需要单独的多处理和多线程呢?如果您尝试使用多线程来提高CPU绑定任务的性能,而当进程数超过某个数值时,您可能会注意到,实际上得到的是性能下降。让我们看看为什么会这样。
因为Python也带有全局解释器锁(GIL)。Python会很乐意让用户生成任意数量的线程,但是GIL确保在任何给定的时间只有一个线程执行。
对于一个io绑定的任务,这完全没问题。一个线程向一个URL发出请求,当它等待响应时,可以将该线程替换为向另一个URL发出另一个请求的另一个线程。因为一个线程在收到响应之前不需要做任何事情,所以在给定的时间内只执行一个线程并不重要。
对于CPU绑定的任务,因为一次只执行一个线程,即使生成多个线程,并且每个线程都有自己的数目来检查素数,CPU仍然一次只处理一个线程。实际上,这些数字仍然会被一个接一个地检查。如果在CPU绑定的任务中使用多线程,那么处理多线程的开销将导致性能下降。
为了克服这个“限制”,我们使用了多处理模块。多处理不是使用线程,而是使用多个进程。每个进程都有自己的解释器和内存空间,因此GIL不会阻止任何事情。本质上,每个进程使用不同的CPU内核同时处理不同的数字。
您可能会注意到,与使用简单的for循环,甚至多线程相比,使用多处理时CPU利用率要高得多。这是因为您的程序使用多个CPU内核,而不仅仅是一个内核。
请记住,多处理本身就有管理多个进程的开销,这通常比多线程开销更大。(多处理生成一个单独的解释器,并为每个进程分配一个单独的内存空间)这意味着,根据经验,当可以使用轻量级多线程时,最好使用它(io绑定任务)。当CPU处理成为瓶颈时,通常需要调用多处理模块。但请记住,能力越大,责任越大。
如果一次生成的进程超过CPU的处理能力,您将注意到性能开始下降。这是因为操作系统现在必须做更多的工作来交换CPU内核内外的进程,因为您的进程比内核多。实际情况可能比简单的解释要复杂得多,但这是基本思想。当我们达到16个进程时,您可以看到我的系统性能下降。这是因为我的CPU只有16个逻辑核心。
4,总结
对于io绑定的任务,使用多线程可以提高性能。
对于io绑定的任务,使用多处理也可以提高性能,但是开销往往比使用多线程高。
Python GIL意味着在Python程序的任何给定时间内只能执行线程。
对于CPU绑定的任务,使用多线程实际上会降低性能。
对于CPU绑定的任务,使用多处理可以提高性能。