社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

基于Python的多进程和多线程对比和应用分析

一个普普通通简简单单平平凡凡的神 • 8 年前 • 834 次点击  

前言

在多核CPU的时代,使用多线程或多进程能够充分利用CPU多核性能来提高程序的执行效率,但Python的多线程为什么有时候耗时比单一线程更长,为什么多数情况下推荐使用Python多进程替代多线程,针对这些问题本文将重点介绍下Python的多进程和多线程区别和应用场景的选取。


进程和线程介绍

程序为存储在磁盘上的可执行文件,当把程序加载到内存中并被操作系统调用,则拥有了生命周期,进程即为运行中的程序。一个进程可以并行运行多个线程,每个线程执行不同的任务,也就是说线程是进程的组成部分。当一个进程启动时至少要执行一个任务,因此至少有一个主线程,由主线程再创建其他的子线程。多线程的执行方式和多进程相似,由操作系统在多个线程之间快速切换,让每个线程都短暂的交替运行,看起来像同时执行一样。当然,多核CPU可真正意义上实现多线程或多进程的同时执行。

进程和线程之间存在不同的特点。每个进程拥有自己的地址空间、内存和数据栈,由操作系统管理所有的进程,并为其合理分配执行时间。由于进程间资源相互独立,不同进程之间需要通过IPC(进程间通信)方式共享信息,但单个进程崩溃时不会导致系统崩溃。而多线程是在同一个进程下执行的,共享同一片数据空间,相比于进程而言,线程间的信息共享更加容易,但当一个线程崩溃时会导致整个进程崩溃。


Python GIL

Python代码的执行由Python解释器进行控制。目前Python的解释器有多种,如CPython、PyPy、Jython等,其中CPython为最广泛使用的Python解释器。理论上CPU是多核时支持多个线程同时执行,但在Python设计之初考虑到在Python解释器的主循环中执行Python代码,于是CPython中设计了全局解释器锁GIL(Global Interpreter Lock)机制()用于管理解释器的访问,Python线程的执行必须先竞争到GIL权限才能执行。因此无论是单核还是多核CPU,任意给定时刻只有一个线程会被Python解释器执行,这也是为什么在多核CPU上,Python的多线程有时效率并不高的根本原因。

注:关于Python解释器,简单的说,任何一种编程语言都需要用另一种语言来实现它,比如C语言是用机器语言来实现的。所以,Python作为一门编程语言,根据实现方式不同分为了CPyhton、Pypy、Jython等。


执行对比

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的其他内核加入执行。


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/8g8kK2lJxs
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/8524