原文链接
Multiprocessing V.S. Threading
如果你不想读整篇文章,这里有你所需要的本文精华:
- 如果你的程序运行效率瓶颈在于网络传输时延,那么你可以使用多线程。
- 如果你的程序运行效率瓶颈在于CPU数量,那么你就可以尝试多进程。
之所以写出这一部分指引是因为我发现在其他关于多线程与多进程的区别的介绍中,大部分信息都十分晦涩难懂。而我们往往需要花很多时间去研究得很深入,到头来发现对自己如何去解决自己手上的实际问题还是没有什么帮助。
我们都知道,python是一个线性的语言,然而当你需要更大的处理性能时,python多线程模块就有他的作用了。即使Python无法用于CPU的并行计算,他在处理I/O操作时仍然很棒,比如在做多线程爬虫的时候,大部分时间处理器都处于空闲状态等待数据的进入。
python多线程实际上改变了多线程的规则,这是因为许多脚本都会花大部分的时间处理与网络/数据之间的I/O或等待数据从远端传入。由于下载的数据不需要是连续的(比如爬取不同的网站信息),处理器就能够并行地从不同数据源下载数据最后把它们拼接起来。然而对于比较集中的CPU操作,threading模块可能就没有那么大的收益了。
我们可以直接从python的标准库中引入多线程模块threading:
import threading
from queue import Queue
import time
同时你可以通过target
参数定义线程调用的函数,通过args
为该函数传参,用start
方法启动线程,如下demo:
def testThread(num):
print(num)
if __name__ == '__main__':
for i in range(5):
t = threading.Thread(target=testThread, arg=(i, ))
t.start()
如果你从没见过if __name__ == '__main__':
这条语句:这条语句是一个能够保证代码在内部嵌套的基本的方法,他只会在你直接运行该脚本的时候执行,而不会在被import时调用。
通常情况下,我们希望多线程能够使用或更改同一个变量的时候不发生冲突,这个时候我们就会用到锁机制。任何时候当一个函数需要更改一个变量的数值,他就会锁住这个变量,而当其他变量需要使用时,必须等待这个变量被当前锁住他的线程释放出来。
假如有两个函数需要迭代地处理一个变量。这个时候锁机制就能够保证一次只能有一个函数获得这个变量的使用权限,对这个变量进行相关的操作,并且在他未释放这个变量之前都不会有其他函数具有访问权限。
在使用python threading模块时,在使用打印功能时经常会发生文本输出的混淆(数据也可能会损坏)。这个时候可以尝试用打印锁来保证一次只有一个线程在进行打印操作。
print_lock = threading.Lock()
def threadTest():
# when this exits, the print_lock is released
with print_lock:
print(worker)
def threader():
while True:
# get the job from the front of the queue
threadTest(q.get())
q.task_done()
q = Queue()
for x in range(5):
thread = threading.Thread(target = threader)
# this ensures the thread will die when the main thread dies
# can set t.daemon to False if you want it to keep running
t.daemon = True
t.start()
for job in range(10):
q.put(job)
上面这个例子我们定义了10个需要完成的任务以及5个线程去处理他们。
现在很多关于多线程的介绍都会忽略掉使用这些方法时的缺点。然而了解多线程在使用过程中的优点和缺点其实是非常重要的:
在没有多进程(Multi-processing)的概念时,由于GIL
(Global Interpreter Lock)机制的影响(详解GIL),python无法最大化系统的性能。实际上,由于python非常古老,开发者在开发的过程时并没有考虑过会使用在多cpu上,在当时的情况下,因为python并不是thread-safe的,所以需要有一个在访问python对象时的全局锁。即使这个GIL锁并不是完美的,但是它确实是一种非常有效的内存管理机制。
多进程能够让程序绕过GIL锁,去并行地处理程序,并能够更充分地使用cpu。虽然它与threading模块本质不同,但是语法上非常相似。多进程库会为每个进程提供各自的解释器和GIL锁。
在多线程上会发生的问题(如数据混淆、死锁等)在多进程上并不会发生。这是因为在多线程上,不同的线程直接的存储不共享,因此也就不会发生同时不同空间同时更改同一内存空间这一情况。
import multiprocessing
def spawn():
print('test!')
if __name__ == '__main__':
for i in range(5):
p = multiprocessing.Process(target=spawn)
p.start()
如果你有一个共享的数据库,你需要确保在开始新流程之前等待相关流程完成。
这时只需要在上面的程序中进行一点小小的改动:
for i in range(5):
p = multiprocessing.Process(target=spawn)
p.start()
p.join() # this line allows you to wait for processes
如果需要往函数中传参,方法也很简单:
import multiprocessing
def spawn(num):
print(num)
if __name__ == '__main__':
for i in range(25):
## right here
p = multiprocessing.Process(target=spawn, args=(i,))
p.start()
这时一个很好的例子,因为没有了p.join()
,这个程序的输出并不是按照你既定的顺序。
跟多线程一样,多进程同样也有一些自己的缺点。当你在选择的时候需要参考自己的场景及他们二者各自的缺点来确定该用哪一种模式: