Python GIL 系列之再谈Python的GIL

简介:  1. 之前写过一篇《通过实例认识Python的GIL》的文章,感觉有些意犹未尽  2. 这次对例子作了些扩展,进一步的分析GIL对Python程序的影响 2.

 1. 之前写过一篇《通过实例认识Python的GIL》的文章,感觉有些意犹未尽 

2. 这次对例子作了些扩展,进一步的分析GIL对Python程序的影响

2.1 先来看例子:

[python]  view plain  copy
  1. from threading import Thread  
  2.   
  3. from threading import Event as TEvent  
  4.   
  5. from multiprocessing import Process  
  6.   
  7. from multiprocessing import Event as PEvent  
  8.   
  9. from timeit import Timer  
  10.   
  11. def countdown(n,event):  
  12.   
  13.     while n > 0:  
  14.   
  15.         n -= 1  
  16.   
  17.     event.set()  
  18.   
  19. def io_op(n,event):  
  20.   
  21.     f = open('test.txt','w')  
  22.   
  23.     while not event.is_set():  
  24.   
  25.         f.write('hello,world')  
  26.   
  27.     f.close()  
  28.   
  29. def t1():  
  30.   
  31.     COUNT=100000000  
  32.   
  33.     event = TEvent()  
  34.   
  35.     thread1 = Thread(target=countdown,args=(COUNT,event))  
  36.   
  37.     thread1.start()  
  38.   
  39.     thread1.join()  
  40.   
  41. def t2():  
  42.   
  43.     COUNT=100000000  
  44.   
  45.     event = TEvent()  
  46.   
  47.     thread1 = Thread(target=countdown,args=(COUNT//2,event))  
  48.   
  49.     thread2 = Thread(target=countdown,args=(COUNT//2,event))  
  50.   
  51.     thread1.start(); thread2.start()  
  52.   
  53.     thread1.join(); thread2.join()  
  54.   
  55. def t3():  
  56.   
  57.     COUNT=100000000  
  58.   
  59.     event = PEvent()  
  60.   
  61.     p1 = Process(target=countdown,args=(COUNT//2,event))  
  62.   
  63.     p2 = Process(target=countdown,args=(COUNT//2,event))  
  64.   
  65.     p1.start(); p2.start()  
  66.   
  67.     p1.join(); p2.join()  
  68.   
  69. def t4():  
  70.   
  71.     COUNT=100000000   
  72.   
  73.     event = TEvent()  
  74.   
  75.     thread1 = Thread(target=countdown,args=(COUNT,event))  
  76.   
  77.     thread2 = Thread(target=io_op,args=(COUNT,event))  
  78.   
  79.     thread1.start(); thread2.start()  
  80.   
  81.     thread1.join(); thread2.join()  
  82.   
  83. def t5():  
  84.   
  85.     COUNT=100000000   
  86.   
  87.     event = PEvent()  
  88.   
  89.     p1 = Process(target=countdown,args=(COUNT,event))  
  90.   
  91.     p2 = Process(target=io_op,args=(COUNT,event))  
  92.   
  93.     p1.start(); p2.start()  
  94.   
  95.     p1.join(); p2.join()  
  96.   
  97. if __name__ == '__main__':  
  98.   
  99.     t = Timer(t1)  
  100.   
  101.     print('countdown in one thread:%f'%(t.timeit(1),))  
  102.   
  103.     t = Timer(t2)  
  104.   
  105.     print('countdown use two thread:%f'%(t.timeit(1),))  
  106.   
  107.     t = Timer(t3)  
  108.   
  109.     print('countdown use two Process:%f'%(t.timeit(1),))  
  110.   
  111.     t = Timer(t4)  
  112.   
  113.     print('countdown in one thread with io op in another thread:%f'%(t.timeit(1),))  
  114.   
  115.     t = Timer(t5)  
  116.   
  117.     print('countdown in one process with io op in another process:%f'%(t.timeit(1),))  


 

 

 

2.2 再来看输出:

 

2.2.1 先来看多核CPU禁用其它CPU,只运行一个CPU,Windows系统,Python2.7.6上运行的结果:

[plain]  view plain  copy
  1. countdown in one thread:', 5.9650638561501195  
  2.   
  3. countdown use two thread:', 5.8188333656781595  
  4.   
  5. countdown use two Process', 6.197559396296269  
  6.   
  7. countdown in one thread with io op in another thread:', 11.369204522553051  
  8.   
  9. countdown in one process with io op in another process:', 11.79234388645473  

 

 

2.2.2 再来看下四核CPU,Windows系统,Python2.7.6上运行的结果:

[plain]  view plain  copy
  1. countdown in one thread:6.479085  
  2.   
  3. countdown use two thread:24.266131  
  4.   
  5. countdown use two Process4.360930  
  6.   
  7. countdown in one thread with io op in another thread:29.967870  
  8.   
  9. countdown in one process with io op in another process:6.478644  

 

2.2.3 再来看下四核64位CPU,Widonws系统,Python3.4上运行的结果:

[html]  view plain  copy
  1. countdown in one thread:12.333187  
  2.   
  3. countdown use two thread:19.358091  
  4.   
  5. countdown use two Process:7.105101  
  6.   
  7. countdown in one thread with io op in another thread:10.443203  
  8.   
  9. countdown in one process with io op in another process:18.682883  
[html]  view plain  copy
  1.    
[html]  view plain  copy
  1.    

 

为了方便对比,还是上张图吧: 

 

1)、单线程

2)、两线程

3)、二进程

4)、CPU计算线程+I/O线程

5)、CPU计算进程+I/O进程

 

2.3 总结:

 

2.3.1 在单核CPU上,一切都很美好

 

单线程和多线程的运行效率差不多。

 

多进程和多线程的表现一致,多进程稍微慢些,可能是进程切换更耗时间所致。

 

CPU和IO混合操作时,多进程和多线程的表现也一致

 

2.3.2 但是到了多核CPU上时,多线程和多进程的区别就暴露无余了:

 

使用多线程情况下,执行同样的计算量,CPU的计算时间比单线程慢了四倍(6.479085比24.266131)

 

如果是多线程情况下的CPU计算和IO混合操作,情况变得更糟(29.967870秒),这里的时间还算是好的,如果开的程序多了,其它程序也在执行IO操作,所耗的时间还会更多。

 

多进程情况下,一切依然美好。

 

2.3.3 在Python3.4上,由于对GIL作了很大的优化,多线程情况下的运行效率有了很大 改善,I/O操作对CPU计算的影响也比较小了,而没有3.2版本上那么大了,但是整体的运行速度比Python2.7慢了2倍!

 

3. 从上面这个例子来看,情况是非常不妙的,Python在多核CPU的情况下,Thread似乎变得一无是处,但是要不要这么悲观呢?我们来接着看下一个例子:

 

4. 我们对原来例子作下优化,将countdown移到c代码中进行处理:

 

4.1 先看代码:

 

4.1.1 utility.pyx

[python]  view plain  copy
  1. def countdown(int n):  
  2.   
  3.     with nogil:  
  4.   
  5.         while n > 0:  
  6.   
  7.             n -= 1  


 

 

4.1.2 Setup.py

[python]  view plain  copy
  1. from distutils.core import setup  
  2.   
  3. from distutils.extension import Extension  
  4.   
  5. from Cython.Build import cythonize  
  6.   
  7. ext = Extension("utility",   
  8.   
  9.                     define_macros = [('MAJOR_VERSION''1'),  
  10.   
  11.                                      ('MINOR_VERSION''0')],  
  12.   
  13.                     sources = ["utility.pyx", ])  
  14.   
  15. setup(  
  16.   
  17.     name = 'callback',  
  18.   
  19.     version = '1.0',  
  20.   
  21.     description = 'This is a callback demo package',  
  22.   
  23.     author = '',  
  24.   
  25.     author_email = 'shi19@163.com',  
  26.   
  27.     url = '',  
  28.   
  29.     long_description = '',  
  30.   
  31.     ext_modules=cythonize([ext,]),  
  32.   
  33. )  
  34.   
  35.    


 

 

4.1.3 count.py

[python]  view plain  copy
  1. from threading import Thread  
  2.   
  3. from threading import Event as TEvent  
  4.   
  5. from multiprocessing import Process  
  6.   
  7. from multiprocessing import Event as PEvent  
  8.   
  9. import utility  
  10.   
  11. from timeit import Timer  
  12.   
  13. def countdown(n,event):  
  14.   
  15.     for i in range(100):  
  16.   
  17.         utility.countdown(n)  
  18.   
  19.     event.set()  
  20.   
  21. def io_op(n,event):  
  22.   
  23.     f = open('test.txt','w')  
  24.   
  25.     while not event.is_set():  
  26.   
  27.         f.write('hello,world')  
  28.   
  29.     f.close()  
  30.   
  31. def t1():  
  32.   
  33.     COUNT=100000000  
  34.   
  35.     event = TEvent()  
  36.   
  37.     thread1 = Thread(target=countdown,args=(COUNT,event))  
  38.   
  39.     thread1.start()  
  40.   
  41.     thread1.join()  
  42.   
  43. def t2():  
  44.   
  45.     COUNT=100000000  
  46.   
  47.     event = TEvent()  
  48.   
  49.     thread1 = Thread(target=countdown,args=(COUNT//2,event))  
  50.   
  51.     thread2 = Thread(target=countdown,args=(COUNT//2,event))  
  52.   
  53.     thread1.start(); thread2.start()  
  54.   
  55.     thread1.join(); thread2.join()  
  56.   
  57. def t3():  
  58.   
  59.     COUNT=100000000  
  60.   
  61.     event = PEvent()  
  62.   
  63.     p1 = Process(target=countdown,args=(COUNT//2,event))  
  64.   
  65.     p2 = Process(target=countdown,args=(COUNT//2,event))  
  66.   
  67.     p1.start(); p2.start()  
  68.   
  69.     p1.join(); p2.join()  
  70.   
  71. def t4():  
  72.   
  73.     COUNT=100000000   
  74.   
  75.     event = TEvent()  
  76.   
  77.     thread1 = Thread(target=countdown,args=(COUNT,event))  
  78.   
  79.     thread2 = Thread(target=io_op,args=(COUNT,event))  
  80.   
  81.     thread1.start(); thread2.start()  
  82.   
  83.     thread1.join(); thread2.join()  
  84.   
  85. def t5():  
  86.   
  87.     COUNT=100000000   
  88.   
  89.     event = PEvent()  
  90.   
  91.     p1 = Process(target=countdown,args=(COUNT,event))  
  92.   
  93.     p2 = Process(target=io_op,args=(COUNT,event))  
  94.   
  95.     p1.start(); p2.start()  
  96.   
  97.     p1.join(); p2.join()  
  98.   
  99. if __name__ == '__main__':  
  100.   
  101.     t = Timer(t1)  
  102.   
  103.     print('countdown in one thread:%f'%(t.timeit(1),))  
  104.   
  105.     t = Timer(t2)  
  106.   
  107.     print('countdown use two thread:%f'%(t.timeit(1),))  
  108.   
  109.     t = Timer(t3)  
  110.   
  111.     print('countdown use two Process:%f'%(t.timeit(1),))  
  112.   
  113.     t = Timer(t4)  
  114.   
  115.     print('countdown in one thread with io op in another thread:%f'%(t.timeit(1),))  
  116.   
  117.     t = Timer(t5)  
  118.   
  119.     print('countdown in one process with io op in another process:%f'%(t.timeit(1),))  

 

 

4.2 几点说明:

 

4.2.1 utility.pyx是cython的脚本,用cython可以实现python和c的混合编程,并可以最终生成c文件。其中with nogil的意思是,在执行while循环的时候释放gil,因为接下来的计算不涉及到Python对象的操作,可以放心大胆的把gil的枷锁给去掉。

 

4.2.2 Setup.py是utility.pyx的编译脚本,执行python Setup.py build_ext --inplace即可在Windows下生成utility.pyd的动态库,在linux下生成的动态库叫utility.so,在python代码中就可以通过import utility来引用扩展。

 

4.2.3 count.py,修改后的测试程序,请注意countdown,这里将会比原来的代码多调用100次countdown!!

 

4.3 运行后得到输出:

[python]  view plain  copy
  1. countdown in one thread:16.968686  
  2.   
  3. countdown use two thread:9.333422  
  4.   
  5. countdown use two Process:9.620321  
  6.   
  7. countdown in one thread with io op in another thread:17.754015  
  8.   
  9. countdown in one process with io op in another process:17.867098  

 

4.4 嗯,世界又变得很美好了,请记住,上面的输出是countdown比原来多调用100倍的输出结果,可见将数字计算的操作移到c代码会获得怎么的性能提升!!

 

5. 好了,最后来作个总结:

 

5.1 Python的GIL在单核情况下对性能的影响可以忽略不计,几乎没有。

 

5.2 Python由于其GIL的存在在多核CPU的情况下Thread的表现真的是非常的糟糕,但是Process则不受GIL的影响。

 

5.3 Python内置的数据类是不适合用于大量的数学计算的,当然这也不仅仅是Python的问题,其它完全面向对象的语言都有这个问题, 要进行大量的数学计算就要用把代码移到C/C++中去实现,这样不仅可以去除gil的影响,更可以让性能获得几十倍上百倍的提升, 或者用numpy之类的扩展在执行科学计算时也可以让性能大幅的提升。

 

5.4 Python慢其实就是慢在数字计算上,想想就知道,如果每一个数字都是一个对象, 在计算的时候就免不了不断的为对象申请内存,释放内存,速度肯定就慢下来。

 

5.5 但是,Python对数据结构的操作是非常高效的,像Python内置的强大的dict,str,list等类, 不是说大话,其处理的速度真的可以和C媲美,因为它们的实现本身就是用C实现的。 我们在编程刚入门的时候就被告知:数据结构+算法=程序,这个道理也许只会在用Python这样的语言时才会有更切身的体会。

 

5.6 在用Python开发程序时,你不得不花点时间在性能优化上来, 过程也很简单:用cProfile类查找出比较耗时的操作,然后将其移到C中去实现, 另外,如果是使用多核CPU的情况,一定要小心使用Thread,尽量用Process来替代Thread,通过本文对GIL的分析,将对性能的优化提供很好的帮助。 其实,Python的性能优化过程也是程序开发中有挑战又非常有成就感的部分。

 

5.7 但是,记住一点,不要过早的对程序进行优化,过早优化是罪恶之源 ---Donald Knuth。前期开发应该把注意力放在功能实现以及代码的可读性和可维护性上来。

 

5.8 最后,愿以一句话作为本篇文件的结束语:都说爱一个人就要爱他(她)的全部,包括他(她)的缺点,对人如此,对物呢?

 示例代码

(完)

目录
相关文章
|
6月前
|
分布式计算 并行计算 安全
在Python Web开发过程中:详述Python中的GIL及其对多线程的影响。
Python的GIL是CPython中的全局锁,限制了多线程并行执行,尤其在CPU密集型任务上导致性能瓶颈。虽然GIL限制了多线程的并行计算,但在I/O密集型任务中,线程可交替执行提升吞吐量。为利用多核,开发者常选择多进程或使用无GIL的解释器,如Jython、PyPy。异步IO和分布式计算也是应对策略。
78 9
|
6月前
|
并行计算 Python
python并发编程: Python速度慢的罪魁祸首,全局解释器锁GIL
python并发编程: Python速度慢的罪魁祸首,全局解释器锁GIL
60 1
python并发编程: Python速度慢的罪魁祸首,全局解释器锁GIL
|
6月前
|
开发框架 并行计算 安全
|
6月前
|
安全 Java C++
解释Python中的全局解释器锁(GIL)和线程安全的概念。
解释Python中的全局解释器锁(GIL)和线程安全的概念。
61 0
|
5月前
|
分布式计算 并行计算 安全
在Python Web开发中,Python的全局解释器锁(Global Interpreter Lock,简称GIL)是一个核心概念,它直接影响了Python程序在多线程环境下的执行效率和性能表现
【6月更文挑战第30天】Python的GIL是CPython中的全局锁,限制了多线程并行执行,尤其是在多核CPU上。GIL确保同一时间仅有一个线程执行Python字节码,导致CPU密集型任务时多线程无法充分利用多核,反而可能因上下文切换降低性能。然而,I/O密集型任务仍能受益于线程交替执行。为利用多核,开发者常选择多进程、异步IO或使用不受GIL限制的Python实现。在Web开发中,理解GIL对于优化并发性能至关重要。
61 0
|
1月前
|
Java C语言 Python
解析Python中的全局解释器锁(GIL):影响、工作原理及解决方案
解析Python中的全局解释器锁(GIL):影响、工作原理及解决方案
46 0
|
3月前
|
安全 Python
Python 中的全局解释器锁(GIL)详解
【8月更文挑战第24天】
91 0
|
5月前
|
开发框架 并行计算 安全
Python的GIL限制了CPython在多核下的并行计算,但通过替代解释器(如Jython, IronPython, PyPy)和多进程、异步IO可规避
【6月更文挑战第26天】Python的GIL限制了CPython在多核下的并行计算,但通过替代解释器(如Jython, IronPython, PyPy)和多进程、异步IO可规避。Numba、Cython等工具编译优化代码,未来社区可能探索更高级的并发解决方案。尽管GIL仍存在,现有策略已能有效提升并发性能。
69 3
|
5月前
|
安全 Java Python
GIL是Python解释器的锁,确保单个进程中字节码执行的串行化,以保护内存管理,但限制了多线程并行性。
【6月更文挑战第20天】GIL是Python解释器的锁,确保单个进程中字节码执行的串行化,以保护内存管理,但限制了多线程并行性。线程池通过预创建线程池来管理资源,减少线程创建销毁开销,提高效率。示例展示了如何使用Python实现一个简单的线程池,用于执行多个耗时任务。
45 6
|
5月前
|
安全 Java Python
Python全局解释器锁(Global Interpreter Lock,简称 GIL), 互斥锁
Python全局解释器锁(Global Interpreter Lock,简称 GIL), 互斥锁
下一篇
无影云桌面