- Preface 前言
- 第1章 引论
- 第2章 编程惯用法
- 第3章 基础语法
- 建议19:有节制地使用 from…import 语句
- 建议20:优先使用 absolute import 来导入模块
- 建议21:i+=1 不等于 ++i
- 建议22:使用 with 自动关闭资源
- 建议23:使用 else 子句简化循环(异常处理)
- 建议24:遵循异常处理的几点基本原则
- 建议25:避免 finally 中可能发生的陷阱
- 建议26:深入理解 None 正确判断对象是否为空
- 建议27:连接字符串应优先使用 join 而不是 +
- 建议28:格式化字符串时尽量使用 .format 方式而不是 %
- 建议29:区别对待可变对象和不可变对象
- 建议30:[]、() 和 {}:一致的容器初始化形式
- 建议31:记住函数传参既不是传值也不是传引用
- 建议32:警惕默认参数潜在的问题
- 建议33:慎用变长参数
- 建议34:深入理解 str() 和 repr() 的区别
- 建议35:分清 staticmethod 和 classmethod 的适用场景
- 第4章 库
- 建议36:掌握字符串的基本用法
- 建议37:按需选择 sort() 或者 sorted()
- 建议38:使用 copy 模块深拷贝对象
- 建议39:使用 Counter 进行计数统计
- 建议40:深入掌握 ConfigParser
- 建议41:使用 argparse 处理命令行参数
- 建议42:使用 pandas 处理大型 CSV 文件
- 建议43:一般情况使用 ElementTree 解析 XML
- 建议44:理解模块 pickle 优劣
- 建议45:序列化的另一个不错的选择 JSON
- 建议46:使用 traceback 获取栈信息
- 建议47:使用 logging 记录日志信息
- 建议48:使用 threading 模块编写多线程程序
- 建议49:使用 Queue 使多线程编程更安全
- 第5章 设计模式
- 第6章 内部机制
- 建议54:理解 built-in objects
- 建议55:init() 不是构造方法
- 建议56:理解名字查找机制
- 建议57:为什么需要 self 参数
- 建议58:理解 MRO 与多继承
- 建议59:理解描述符机制
- 建议60:区别 getattr() 和 getattribute() 方法
- 建议61:使用更为安全的 property
- 建议62:掌握 metaclass
- 建议63:熟悉 Python 对象协议
- 建议64:利用操作符重载实现中缀语法
- 建议65:熟悉 Python 的迭代器协议
- 建议66:熟悉 Python 的生成器
- 建议67:基于生成器的协程及 greenlet
- 建议68:理解 GIL 的局限性
- 建议69:对象的管理与垃圾回收
- 第7章 使用工具辅助项目开发
- 第8章 性能剖析与优化
建议68:理解 GIL 的局限性
在Python多线程编程中,你有没有遇到过这种问题:多线程Python程序运行的速度比只有一个线程的时候还要慢?除了程序本身的并行性之外,很大程度上与GIL有关。GIL在Python中是一个很有争议的话题,由于它的存在,多线程编程在Python中似乎并不理想,为什么这么说呢?先来了解一下GIL。GIL被称为为全局解释器锁(Global Interpreter Lock),是Python虚拟机上用作互斥线程的一种机制,它的作用是保证任何情况下虚拟机中只会有一个线程被运行,而其他线程都处于等待GIL锁被释放的状态。对于有I/O操作的多线程,其线程执行状态如图6-6所示。不管是在单核系统还是多核系统中,始终只有一个获得了GIL锁的线程在运行,每次遇到I/O操作便会进行GIL锁的释放。
但如果是纯计算的程序,没有I/O操作,解释器则会根据sys.setcheckinterval的设置来自动进行线程间的切换,默认情况下每隔100个时钟(注:这里的时钟指的是Python的内部时钟,对应于解释器执行的指令)就会释放GIL锁从而轮换到其他线程的执行,示意图如图6-7所示。
图6-6 Python虚拟机中I/O操作中GIL的变换过程
图6-7 无I/O操作时GIL的变换过程
在单核CPU中,GIL对多线程的执行并没有太大影响,因为单核上的多线程本质上就是顺序执行的。但对于多核CPU,多线程并不能真正发挥优势带来效率上明显的提升,甚至在频繁I/O操作的情况下由于存在需要多次释放和申请GIL的情形,效率反而会下降。那么,有人不禁会问:Python解释器中为什么要引入GIL呢?来思考这样一个情形:我们知道Python中对象的管理与引用计数器密切相关,当计数器变为0的时候,该对象便会被垃圾回收器回收。当撤销对一个对象的引用时,Python解释器对对象以及其计数器的管理分为以下两步:
1)使引用计数值减1。
2)判断该计数值是否为0,如果为0,则销毁该对象。
假设线程A和B同时引用同一个对象obj,这时obj的引用计数值为2。如果现在线程A打算撤销对obj的引用。当执行完第一步的时候,由于存在多线程调度机制,A恰好在这个关键点被挂起,而B进入执行状态,如图6-8所示。但不幸的是B也同样做了撤销对obj的引用的动作,并顺利完成了所有两个步骤,这个时候由于obj的引用计数器为0,因此对象被销毁,内存被释放。但如果此时A再次被唤醒去执行第二步操作的时候会发现已经面目全非,则其操作结果完全未知。
图6-8 无GIL存在时线程的同步
鉴于此,在Python解释器中引入了GIL,以保证对虚拟机内部共享资源访问的互斥性。GIL的引入确实使得多线程不能在多核系统中发挥优势,但它也带来了一些好处:大大简化了Python线程中共享资源的管理,在单核CPU上,由于其本质是顺序执行的,一般情况下多线程能够获得较好的性能。此外,对于扩展的C程序的外部调用,即使其不是线程安全的,但由于GIL的存在,线程会阻塞直到外部调用函数返回,线程安全不再是一个问题。
多核CPU已经成为一个常见的现象,GIL的局限性限制了其在多核CPU上发挥优势,因此对于GIL的去留也曾引发过激烈的讨论。Guido以及Python的开发人员都有一个很明确的解释,那就是去掉GIL并不容易。实际上在1999年,针对Python1.5,Greg Stein发布了一个补丁,该补丁中GIL被完全移除,使用高粒度的锁来代替。然而这种解决方案并没有带来理想的效果,多核多线程速度的提升并没有随着核数的增加而线性增长,反而给单线程程序的执行速度带来了一定的代价,当用单线程执行时,速度大约降低了40%。因此,这种方案最终也被放弃。在Python3.2中重新实现了GIL,其实现机制主要集中在两个方面:一方面是使用固定的时间而不是固定数量的操作指令来进行线程的强制切换;另一个方面是在线程释放GIL后,开始等待,直到某个其他线程获取GIL后,再开始去尝试获取GIL,这样虽然可以避免此前获得GIL的线程,不会立即再次获取GIL,但仍然无法保证优先级高的线程优先获取GIL。这种方式只能解决部分问题,并未改变GIL的本质,GIL本质上的改观目前并没有非常明朗的前景。不过也不需要那么悲观,Python提供了其他方式可以绕过GIL的局限,比如使用多进程multiprocessing模块或者采用C语言扩展的方式,以及通过ctypes和C动态库来充分利用物理内核的计算能力。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论