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. python从字符串解析方法名

    方法如下 import requests func_name = 'get' fn_obj = getattr(requests,func_name) fn_obj('http://www.baidu ...

  2. Android广播接收器和Activity间传递数据

    Activity向广播接收器传递数据很简单,只需要在发送广播前将数据put进Intent中就行了. 广播接收器怎么向Activity传送数据?这里要用到接口,通过在广播接收器里定义一个接口,然后让接收 ...

  3. WPF基础学习笔记整理 (四) 布局

    WPF使用的是容器(container)进行布局: WPF窗口(Window类型)只能包含单个元素,故为了放置多个元素并增强界面效果,引入了容器: WPF布局容器都派生自System.Windows. ...

  4. xss脚本注入后端的防护

    1.脚本注入最主要的是不要相信用户输入的任何数据,对数据进行转义 可以使用:org.springframework.web.util.HtmlUtils包中的 HtmlUtils.htmlEscape ...

  5. python sort、sorted

    1. (1).sorted()方法返回一个新列表(默认升序). list.sort() (2).另一个不同:list.sort()方法仅被定义在list中,sorted()方法对所有的可迭代序列都有效 ...

  6. C++创建虚拟机调用JAVA类

    ZC: 简要摘抄: “ 1.      Object类出创建JVM. 使用Java类之前必须要创建JVM环境.JDK由java.exe来完成.本文有Object类的静态方法BeginJVM来创建,用E ...

  7. C++指针总结

    在C++中通过动态创建的对象,我们只能获得一个指针,并通过指针控制它.指针是存放对象的内存地址值,更准确的描述是对象的起始地址值.每一个指针都有一个相关的类型,不同数据类型的指针之间的区别不在指针的描 ...

  8. Angular2,Springboot,Zuul,Shiro跨域CORS请求踩坑实录

    前言:前后端分离,业务分离,网关路由等已经成为当下web application开发的流行趋势.前端以单页面路由为核心的框架为主体,可以单独部署在nodejs或nginx上.后端以springboot ...

  9. Python 爬虫-BeautifulSoup

    2017-07-26 10:10:11 Beautiful Soup可以解析html 和 xml 格式的文件. Beautiful Soup库是解析.遍历.维护“标签树”的功能库.使用Beautifu ...

  10. C#退出模式

    1.this.Close();   只是关闭当前窗口,若不是主窗体的话,是无法退出程序的,另外若有托管线程(非主线程),也无法干净地退出: 2.Application.Exit();  强制所有消息中 ...