在多核 CPU 的时代, 使用多线程或多进程能够充分利用 CPU 多核性能来提高程序的执行效率, 但 Python 的多线程为什么有时候耗时比单一线程更长, 为什么多数情况下推荐使用 Python 多进程替代多线程, 针对这些问题本文将重点介绍下 Python 的多进程和多线程区别和应用场景的选取
进程和线程介绍
程序为存储在磁盘上的可执行文件, 当把程序加载到内存中并被操作系统调用, 则拥有了生命周期, 进程即为运行中的程序一个进程可以并行运行多个线程, 每个线程执行不同的任务, 也就是说线程是进程的组成部分当一个进程启动时至少要执行一个任务, 因此至少有一个主线程, 由主线程再创建其他的子线程多线程的执行方式和多进程相似, 由操作系统在多个线程之间快速切换, 让每个线程都短暂的交替运行, 看起来像同时执行一样当然, 多核 CPU 可真正意义上实现多线程或多进程的同时执行
进程和线程之间存在不同的特点每个进程拥有自己的地址空间内存和数据栈, 由操作系统管理所有的进程, 并为其合理分配执行时间由于进程间资源相互独立, 不同进程之间需要通过 IPC(进程间通信)方式共享信息, 但单个进程崩溃时不会导致系统崩溃而多线程是在同一个进程下执行的, 共享同一片数据空间, 相比于进程而言, 线程间的信息共享更加容易, 但当一个线程崩溃时会导致整个进程崩溃
Python GIL
Python 代码的执行由 Python 解释器进行控制目前 Python 的解释器有多种, 如 CPythonPyPyJython 等, 其中 CPython 为最广泛使用的 Python 解释器理论上 CPU 是多核时支持多个线程同时执行, 但在 Python 设计之初考虑到在 Python 解释器的主循环中执行 Python 代码, 于是 CPython 中设计了全局解释器锁 GIL(Global Interpreter Lock)机制 () 用于管理解释器的访问, Python 线程的执行必须先竞争到 GIL 权限才能执行因此无论是单核还是多核 CPU, 任意给定时刻只有一个线程会被 Python 解释器执行, 这也是为什么在多核 CPU 上, Python 的多线程有时效率并不高的根本原因
注: 关于 Python 解释器, 简单的说, 任何一种编程语言都需要用另一种语言来实现它, 比如 C 语言是用机器语言来实现的所以, Python 作为一门编程语言, 根据实现方式不同分为了 CPyhtonPypyJython 等
执行对比
Python 多任务的解决方案主要由这么几种:
启动多进程, 每个进程只有一个线程, 通过多进程执行多任务;
启动单进程, 在进程内启动多线程, 通过多线程执行多任务;
启动多进程, 在每个进程内再启动多个线程, 同时执行更多的任务;
由于第三种方法模型复杂, 实际较少使用, 本文针对前两种方案在双核 CPU(Intel(R) Core(TM)2 Duo CPU E7500@ 2.93GHz)硬件平台上, Linux 操作系统下使用 Python2.7 对计算密集型和 I/O 密集型任务进行执行效率测试
(1)计算密集型测试计算密集型任务的特点是需要进行大量的计算, 在整个时间片内始终消耗 CPU 的资源由于 GIL 机制的原因多线程中无法利用多核参与计算, 但多线程之间切换的开销时间仍然存在, 因此多线程比单一线程需要更多的执行时间而多进程中有各自独立的 GIL 锁互不影响, 可以充分利用多核参与计算, 加快了执行速度
测试代码如下:
- #!/usr/bin/python
- from threading import Thread
- from multiprocessing import Process,Manager
- from timeit import timeit
- def count(n):
- while n> 0:
- n-=1
- def test_normal():
- count(1000000)
- count(1000000)
- def test_Thread():
- t1 = Thread(target=count,args=(1000000,))
- t2 = Thread(target=count,args=(1000000,))
- t1.start()
- t2.start()
- t1.join()
- t2.join()
- def test_Thread():
- t1 = Thread(target=count,args=(1000000,))
- t2 = Thread(target=count,args=(1000000,))
- t1.start()
- t2.start()
- t1.join()
- t2.join()
- def test_Process():
- t1 = Process(target=count,args=(1000000,))
- t2 = Process(target=count,args=(1000000,))
- t1.start()
- t2.start()
- t1.join()
- t2.join()
- if __name__ == '__main__':
- print "test_normal",timeit('test_normal()','from __main__ import test_normal',number=10)
- print "test_Thread",timeit('test_Thread()','from __main__ import test_Thread',number=10)
- print "test_Process",timeit('test_Process()','from __main__ import test_Process',number=10)
执行结果如下:
- test_normal 1.19412684441
- test_Thread 1.88164782524
- test_Process 0.687992095947
注: Join 函数为逐个执行进程 / 线程, 直到子进程 / 线程结束后主进程 / 线程才退出
(2)I/O 密集型测试 I/O 密集型任务的特点是 CPU 消耗很少, 任务大部分时间都在等待 I/O 操作的完成 (I/O 速度远低于 CPU 和内存速度) 此处将 count()函数内容替换为 time.sleep(0.5), 使用挂起方式模拟 I/O 阻塞当挂起时 I/O 任务释放 GIL, 此时允许其他并发线程执行, 提升了运行程序的效率由于多进程创建和销毁的开销比多线程大, 此处创建两个线程和进程时可发现线程执行效率更高, 当线程和进程数量增加至 100 个时差距更加明显
2 个线程和进程测试代执行结果如下:
- test_normal 10.0101339817
- test_Thread 5.00919413567
- test_Process 5.03114795685
增加至 100 个线程和进程测试代码:
- def test_Thread():
- l = []
- for i in range(100):
- p = Thread(target=count,args=(1000000,))
- l.append(p)
- p.start()
- for j in l:
- j.join()
- def test_Process():
- l = []
- for i in range(100):
- p = Process(target=count,args=(1000000,))
- l.append(p)
- p.start()
- for j in l:
- j.join()
执行结果如下:
- test_Thread 5.15325403214
- test_Process 5.84798789024
总结
由于 Python 的 GIL 限制, 多线程更适合于 I/O 密集型应用 (如典型的爬虫程序) 而对于计算密集型的应用, 为了实现更好的并行性, 可使用多进程以使 CPU 的其他内核加入执行
来源: https://juejin.im/entry/5aa6360c518825555e5d7dea