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

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

2.1 先来看例子:

[python] view
plain
 copy

  1. from threading import Thread
  2. from threading import Event as TEvent
  3. from multiprocessing import Process
  4. from multiprocessing import Event as PEvent
  5. from timeit import Timer
  6. def countdown(n,event):
  7. while n > 0:
  8. n -= 1
  9. event.set()
  10. def io_op(n,event):
  11. f = open('test.txt','w')
  12. while not event.is_set():
  13. f.write('hello,world')
  14. f.close()
  15. def t1():
  16. COUNT=100000000
  17. event = TEvent()
  18. thread1 = Thread(target=countdown,args=(COUNT,event))
  19. thread1.start()
  20. thread1.join()
  21. def t2():
  22. COUNT=100000000
  23. event = TEvent()
  24. thread1 = Thread(target=countdown,args=(COUNT//2,event))
  25. thread2 = Thread(target=countdown,args=(COUNT//2,event))
  26. thread1.start(); thread2.start()
  27. thread1.join(); thread2.join()
  28. def t3():
  29. COUNT=100000000
  30. event = PEvent()
  31. p1 = Process(target=countdown,args=(COUNT//2,event))
  32. p2 = Process(target=countdown,args=(COUNT//2,event))
  33. p1.start(); p2.start()
  34. p1.join(); p2.join()
  35. def t4():
  36. COUNT=100000000
  37. event = TEvent()
  38. thread1 = Thread(target=countdown,args=(COUNT,event))
  39. thread2 = Thread(target=io_op,args=(COUNT,event))
  40. thread1.start(); thread2.start()
  41. thread1.join(); thread2.join()
  42. def t5():
  43. COUNT=100000000
  44. event = PEvent()
  45. p1 = Process(target=countdown,args=(COUNT,event))
  46. p2 = Process(target=io_op,args=(COUNT,event))
  47. p1.start(); p2.start()
  48. p1.join(); p2.join()
  49. if __name__ == '__main__':
  50. t = Timer(t1)
  51. print('countdown in one thread:%f'%(t.timeit(1),))
  52. t = Timer(t2)
  53. print('countdown use two thread:%f'%(t.timeit(1),))
  54. t = Timer(t3)
  55. print('countdown use two Process:%f'%(t.timeit(1),))
  56. t = Timer(t4)
  57. print('countdown in one thread with io op in another thread:%f'%(t.timeit(1),))
  58. t = Timer(t5)
  59. 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. countdown use two thread:', 5.8188333656781595
  3. countdown use two Process', 6.197559396296269
  4. countdown in one thread with io op in another thread:', 11.369204522553051
  5. 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. countdown use two thread:24.266131
  3. countdown use two Process4.360930
  4. countdown in one thread with io op in another thread:29.967870
  5. 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. countdown use two thread:19.358091
  3. countdown use two Process:7.105101
  4. countdown in one thread with io op in another thread:10.443203
  5. countdown in one process with io op in another process:18.682883
[html] view
plain
 copy

[html] view
plain
 copy

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

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. with nogil:
  3. while n > 0:
  4. n -= 1

4.1.2 Setup.py

[python] view
plain
 copy

  1. from distutils.core import setup
  2. from distutils.extension import Extension
  3. from Cython.Build import cythonize
  4. ext = Extension("utility",
  5. define_macros = [('MAJOR_VERSION', '1'),
  6. ('MINOR_VERSION', '0')],
  7. sources = ["utility.pyx", ])
  8. setup(
  9. name = 'callback',
  10. version = '1.0',
  11. description = 'This is a callback demo package',
  12. author = '',
  13. author_email = 'shi19@163.com',
  14. url = '',
  15. long_description = '',
  16. ext_modules=cythonize([ext,]),
  17. )

4.1.3 count.py

[python] view
plain
 copy

  1. from threading import Thread
  2. from threading import Event as TEvent
  3. from multiprocessing import Process
  4. from multiprocessing import Event as PEvent
  5. import utility
  6. from timeit import Timer
  7. def countdown(n,event):
  8. for i in range(100):
  9. utility.countdown(n)
  10. event.set()
  11. def io_op(n,event):
  12. f = open('test.txt','w')
  13. while not event.is_set():
  14. f.write('hello,world')
  15. f.close()
  16. def t1():
  17. COUNT=100000000
  18. event = TEvent()
  19. thread1 = Thread(target=countdown,args=(COUNT,event))
  20. thread1.start()
  21. thread1.join()
  22. def t2():
  23. COUNT=100000000
  24. event = TEvent()
  25. thread1 = Thread(target=countdown,args=(COUNT//2,event))
  26. thread2 = Thread(target=countdown,args=(COUNT//2,event))
  27. thread1.start(); thread2.start()
  28. thread1.join(); thread2.join()
  29. def t3():
  30. COUNT=100000000
  31. event = PEvent()
  32. p1 = Process(target=countdown,args=(COUNT//2,event))
  33. p2 = Process(target=countdown,args=(COUNT//2,event))
  34. p1.start(); p2.start()
  35. p1.join(); p2.join()
  36. def t4():
  37. COUNT=100000000
  38. event = TEvent()
  39. thread1 = Thread(target=countdown,args=(COUNT,event))
  40. thread2 = Thread(target=io_op,args=(COUNT,event))
  41. thread1.start(); thread2.start()
  42. thread1.join(); thread2.join()
  43. def t5():
  44. COUNT=100000000
  45. event = PEvent()
  46. p1 = Process(target=countdown,args=(COUNT,event))
  47. p2 = Process(target=io_op,args=(COUNT,event))
  48. p1.start(); p2.start()
  49. p1.join(); p2.join()
  50. if __name__ == '__main__':
  51. t = Timer(t1)
  52. print('countdown in one thread:%f'%(t.timeit(1),))
  53. t = Timer(t2)
  54. print('countdown use two thread:%f'%(t.timeit(1),))
  55. t = Timer(t3)
  56. print('countdown use two Process:%f'%(t.timeit(1),))
  57. t = Timer(t4)
  58. print('countdown in one thread with io op in another thread:%f'%(t.timeit(1),))
  59. t = Timer(t5)
  60. 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. countdown use two thread:9.333422
  3. countdown use two Process:9.620321
  4. countdown in one thread with io op in another thread:17.754015
  5. 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 最后,愿以一句话作为本篇文件的结束语:都说爱一个人就要爱他(她)的全部,包括他(她)的缺点,对人如此,对物呢?

示例代码

(完)

Python GIL 系列之再谈Python的GIL的更多相关文章

  1. Python学习系列(四)Python 入门语法规则2

    Python学习系列(四)Python 入门语法规则2 2017-4-3 09:18:04 编码和解码 Unicode.gbk,utf8之间的关系 2.对于py2.7, 如果utf8>gbk, ...

  2. Python 基础系列一:初识python

    为什么是Python而不是其他语言? C 和 Python.Java.C#等 C语言: 代码编译得到 机器码 ,机器码在处理器上直接执行,每一条指令控制CPU工作. 其他语言: 代码编译得到 字节码 ...

  3. flask开发restful api系列(8)-再谈项目结构

    上一章,我们讲到,怎么用蓝图建造一个好的项目,今天我们继续深入.上一章中,我们所有的接口都写在view.py中,如果几十个,还稍微好管理一点,假如上百个,上千个,怎么找?所有接口堆在一起就显得杂乱无章 ...

  4. Python 基础系列一:初识python(二)基本数据类型

    上节拾遗 1.编码转换过程,utf-8转换gbk 过程 经过解码(py27): x.decode('utf-8')-->unicode-->编码x.encode('gbk') ps:py3 ...

  5. Python fullstack系列【2】Python数据类型

    基本数据类型 学习一门编程语言通常都是先了解其不同的数据类型,以及每种数据类型对象所附带的方法,Python也不例外,本篇就详细介绍下这部分. Python基本数据类型总览: 1.Booleans(布 ...

  6. Python学习系列(三)Python 入门语法规则1

    一.注释 ''' 多行注释 ''' #单行注释 '''    #example1.1 测试程序  时间:4/17/2017 i1=input("请输入用户名:") i2=input ...

  7. JVM系列之:再谈java中的safepoint

    目录 safepoint是什么 safepoint的例子 线程什么时候会进入safepoint safepoint是怎么工作的 总结 safepoint是什么 java程序里面有很多很多的java线程 ...

  8. Hadoop概念学习系列之再谈hadoop集群里的本地模式、伪分布模式和全分布模式(三十七)

    能看懂博主我此博文,相信你已经有了一定基础了. 对于本地模式.伪分布模式和全分布模式的概念,这里,我不多赘述.太多资料和博客,随便在网上一搜就好. 比如<hadoop实战 第二版>陆嘉恒老 ...

  9. Python学习系列:目录

    Python学习系列(二)Python 编译原理简介 Python学习系列(三)Python 入门语法规则1 Python学习系列(四)Python 入门语法规则2

随机推荐

  1. 01_Flume基本架构及原理

    Flume消息收集系统,在整个系统架构中的位置 Flume概况1) Apache软件基金会的顶级项目2)存在两个大的版本:Flume 0.9.x(Flume-OG,original generatio ...

  2. jq 插入结构

    一.插入 1. append $("#div").append('<a href="baidu.com">a</a>') ;   // ...

  3. Codeforces 909C - Python Indentation

    909C - Python Indentation 思路:dp. http://www.cnblogs.com/Leohh/p/8135525.html 可以参考一下这个博客,我的dp是反过来的,这样 ...

  4. html css 伪样式

    伪类的分类及作用: 引自W3School教程伪元素的分类及作用: 这里就不进行多的描述,原文地址:http://www.it165.net/design/html/201406/2643.html

  5. 增加centos7.3上安装php7的php-soap扩展

    代码传到正式服务器上去就:Class 'SoapClient' not found,只能是soap扩展没装!    因为服务器上面的PHP是7.1.11的,所以soap也要装7.1.11的,否则会冲突 ...

  6. 16S 基础知识、分析工具和分析流程详解

    工作中有个真理:如果你连自己所做的工作的来龙去脉都讲不清楚,那你是绝对不可能把这份工作做好的. 这适用于任何行业.如果你支支吾吾,讲不清楚,那么说难听点,你在混日子,没有静下心来工作. 检验标准:随时 ...

  7. LeetCode--058--最后一个单词的长度

    问题描述 给定一个仅包含大小写字母和空格 ' ' 的字符串,返回其最后一个单词的长度. 如果不存在最后一个单词,请返回 0 . 说明:一个单词是指由字母组成,但不包含任何空格的字符串. 示例: 输入: ...

  8. 『PyTorch』第十六弹_hook技术

    由于pytorch会自动舍弃图计算的中间结果,所以想要获取这些数值就需要使用钩子函数. 钩子函数包括Variable的钩子和nn.Module钩子,用法相似. 一.register_hook impo ...

  9. 主席树学习笔记-hdu-2665

    主席树就是对每个历史版本都建了一颗线段树,这样我们在统计一些问题的时候,对于一个区间[L,R]的询问,就可以利用前缀和的思想找到第L-1和第R颗历史版本的线段树来处理查找.由于这样空间需求就增大了,注 ...

  10. SourceTree

    MAC上最好的GIT免费GUI工具是SourceTree(没有之一).此外,最好的GIT代码开源网站是GitHub,最好的GIT代码私有库是BitBucket https://www.sourcetr ...