Python之路(第四十三篇)线程的生命周期、全局解释器锁
一、线程的生命周期(新建、就绪、运行、阻塞和死亡)
当线程被创建并启动以后,它既不是一启动就进入执行状态的,也不是一直处于执行状态的,在线程的生命周期中,它要经过新建(new)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。
尤其是当线程启动以后,它不可能一直“霸占”着 CPU 独自运行,所以 CPU 需要在多个线程之间切换,于是线程状态也会多次在运行、就绪之间转换。
线程的新建和就绪状态
当程序创建了一个 Thread 对象或 Thread 子类的对象之后,该线程就处于新建状态,和其他的Python 对象一样,此时的线程对象并没有表现出任何线程的动态特征,程序也不会执行线程执行体。
当线程对象调用 start() 方法之后,该线程处于就绪状态,Python 解释器会为其创建方法调用栈和程序计数器,处于这种状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于 Python 解释器中线程调度器的调度。
二、全局解释器锁
简单来说,Python全局解释器锁(Global Interpreter Lock)或GIL是一个互斥锁,它只允许一个线程来控制Python解释器。
这意味着在任何时间点只有一个线程可以处于执行状态。执行单线程程序的开发人员感受不到GIL的影响,但它可能是CPU限制型和多线程代码中的性能瓶颈。
由于即使在具有多个CPU核心的多线程架构中,GIL一次也只允许一个线程执行,因此GIL已经成为Python“臭名昭着”的特性。
在CPython中
IO密集型,某个线程阻塞,就会调度其他就绪线程;
CPU密集型,当前线程可能会连续的获得GIL,导致其它线程几乎无法使用CPU。
在CPython中由于有GIL存在,IO密集型,使用多线程较为合算;CPU密集型,使用多进程,要绕开GIL。
新版CPython正在努力优化GIL的问题,但不是移除。 如果在意多线程的效率问题,请绕行,选择其它语言erlang、Go等。
为什么需要GIL
Python使用引用计数进行内存管理。这意味着在Python中创建的对象具有引用计数变量,该变量用于跟踪指向该对象的引用数。当此计数达到零时,释放对象占用的内存。
让我们看一个简短的代码示例来演示引用计数的工作原理:
>>>
>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount (a)
3
在上面的示例中,空列表对象的引用计数为3。列表对象由a,b引用并且参数传递给sys.getrefcount()。
回到GIL:
问题是这个引用计数变量需要保护竞争条件。如果其中两个线程同时增加或减少其值,如果发生这种情况,它可能导致从未释放的内存泄漏,或者更糟糕的是,在对该对象的引用仍然存在时错误地释放内存。这可能会导致Python程序中出现崩溃或其他“怪异”错误。通过向跨线程共享的所有数据结构添加锁,可以保持此引用计数变量的安全性,从而不会对它们进行不一致的修改。
但是为每个对象或对象组添加一个锁意味着将存在多个锁,这可能导致另一个问题 - 死锁(死锁只有在有多个锁时才会发生)。另一个副作用是由于重复获取和释放锁而导致性能下降。
GIL是解释器本身的单个锁,它增加了一条规则,即执行任何Python字节码都需要获取解释器锁。这可以防止死锁(因为只有一个锁)并且不会引入太多的性能开销。但它有效地使任何受CPU限制的Python程序都是单线程的。
注意
需要注意的点包括:
第一,GIL 不属于 Python 语言定义,而是 CPython 解释器实现的一部分;
第二,其他 Python 解释器不一定有 GIL。例如 Jython (JVM) 和 IronPython (CLR) 没有 GIL,而 PyPy 有 GIL;
第三,GIL 并不是 Python 的专利。其他语言也有 GIL,尤其是动态语言,如 Ruby MRI。
在多线程环境中,Python 按以下方式执行:
a、设置 GIL;
b、切换到一个线程去运行;
c、运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0));
d、把线程设置为睡眠状态;
e、解锁 GIL;
d、再次重复以上所有步骤。 在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)编写扩展的程序员可以主动解锁GIL。
至于GIL,不要认为它在那的存在就是静态的和未经分析过的。Antoine Pitrou 在Python 3.2中实现了一个新的GIL,并且带着一些积极的结果。这是自1992年以来,GIL的一次最主要改变。这个改变非常巨大,很难在这里解释清楚,但是从一个更高层次的角度来说,旧的GIL通过对Python指令进行计数来确定何时放弃GIL。这样做的结果就是,单条Python指令将会包含大量的工作,即它们并没有被1:1的翻译成机器指令。在新的GIL实现中,用一个固定的超时时间来指示当前的线程以放弃这个锁。在当前线程保持这个锁,且当第二个线程请求这个锁的时候,当前线程就会在5ms后被强制释放掉这个锁(这就是说,当前线程每5ms就要检查其是否需要释放这个锁)。
然而,这并不是一个完美的改变。对于在各种类型的任务上有效利用GIL这个领域里,最活跃的研究者可能就是David Beazley了。除了对Python 3.2之前的GIL研究最深入,他还研究了这个最新的GIL实现,并且发现了很多有趣的程序方案。对于这些程序,即使是新的GIL实现,其表现也相当糟糕。他目前仍然通过一些实际的研究和发布一些实验结果来引领并推进着有关GIL的讨论。
为什么还没有删除GIL?
Python的开发人员对此有很多抱怨,但是像Python这样流行的语言不会带来像删除GIL那样重要的变化而不会导致向后不兼容问题。
显然可以删除GIL,过去开发人员和研究人员已多次执行此操作,但所有这些尝试都破坏了现有的C扩展,这些扩展在很大程度上依赖于GIL提供的解决方案。
当然,还有其他解决方案可以解决GIL解决的问题,但有些解决方案会降低单线程和多线程I / O绑定程序的性能,其中一些程序太难了。毕竟,你不希望现有的Python程序在新版本发布后运行得更慢,对吧?
Python的创建者和BDFL,Guido van Rossum,在2007年9月的文章“删除GIL并不容易”中给出了社区的答案:
“ 只有当单线程程序(以及多线程但 I / O绑定程序)的性能不降低时,我才欢迎使用Py3k中的一组补丁”
此后的任何尝试都没有实现这一条件。
GIL的解决方案
如果GIL导致您出现问题,可以尝试以下几种方法:
多进程与多线程:最流行的方法是使用多方法,使用多个进程而不是线程。
替代Python解释器: Python有多个解释器实现。分别用C,Java,C#和Python编写的CPython,Jython,IronPython和PyPy是最受欢迎的。GIL仅存在于CPython的原始Python实现中。如果您的程序及其库可用于其他实现之一,那么您也可以尝试它们。
所以没救了么?
当然Python社区也在非常努力的不断改进GIL,甚至是尝试去除GIL。并在各个小版本中有了不少的进步。
将切换颗粒度从基于opcode计数改成基于时间片计数
避免最近一次释放GIL锁的线程再次被立即调度
新增线程优先级功能(高优先级线程可以迫使其他线程释放所持有的GIL锁)
总结
Python GIL其实是功能和性能之间权衡后的产物,它尤其存在的合理性,也有较难改变的客观因素。从本分的分析中,我们可以做以下一些简单的总结:
因为GIL的存在,只有IO Bound场景下得多线程会得到较好的性能
如果对并行计算性能较高的程序可以考虑把核心部分也改成C模块,或者索性用其他语言实现
GIL在较长一段时间内将会继续存在,但是会不断对其进行改进
参考资料
[1] https://www.cnblogs.com/gengcx/p/7500401.html
[2]https://zhuanlan.zhihu.com/p/56352731
Python之路(第四十三篇)线程的生命周期、全局解释器锁的更多相关文章
- Python之路【第十三篇】:jQuery -暂无内容-待更新
Python之路[第十三篇]:jQuery -暂无内容-待更新
- 第四十篇:Vue的生命周期(一)
好家伙,军训结束了,回归 Vue实例的生命周期 1.什么是生命周期? 从Vue实例创建,运行到销毁期间总是伴随着各种各样的事件,这些事件,统称为生命周期. 2.什么是生命周期钩子? 生命周期函数的别称 ...
- Python之路(第三十三篇) 网络编程:socketserver深度解析
一.socketserver 模块介绍 socketserver是标准库中的一个高级模块,用于网络客户端与服务器的实现.(version = "0.4") 在python2中写作S ...
- Python之路(第四十七篇) 协程:greenlet模块\gevent模块\asyncio模块
一.协程介绍 协程:是单线程下的并发,又称微线程,纤程.英文名Coroutine.一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的. 协程相比于线程,最大的区别在于 ...
- Python之路(第四十篇)进程池
一.进程池 进程池也是通过事先划分一块系统资源区域,这组资源区域在服务器启动时就已经创建和初始化,用户如果想创建新的进程,可以直接取得资源,从而避免了动态分配资源(这是很耗时的). 线程池内子进程的数 ...
- Python之路【第二十三篇】:数据库基础
数据库的简介 数据库 数据库(database,DB)是指长期存储在计算机内的,有组织,可共享的数据的集合.数据库中的数据按一定的数学模型组织.描述和存储,具有较小的冗余,较高的数据独立性和易扩展性, ...
- Python之路【第二十三篇】爬虫
difference between urllib and urllib2 自己翻译的装逼必备 What is the difference between urllib and urllib2 mo ...
- Python之路【第二十三篇】:Django 初探--Django的开发服务器及创建数据库(笔记)
Django 初探--Django的开发服务器及创建数据库(笔记) 1.Django的开发服务器 Django框架中包含一些轻量级的web应用服务器,开发web项目时不需再对其配置服务器,Django ...
- Python之路,第十三篇:Python入门与基础13
python3 模块 模块 Module 概念: 模块是一个保护有一系统变量.函数.类等组成的程序组: 模块是一个文件,模块文件名通常以.py 结尾: 作用:让一些相关的变量,函数, 类等有逻辑的 ...
随机推荐
- macos -bash: yarn: command not found/-bash: cnpm: command not found
-bash: cnpm: command not found-bash: yarn: command not found-bash: xxxx: command not found如上yarn/cnp ...
- CUDA10安装配置
https://developer.nvidia.com/cuda-downloads?target_os=Windows&target_arch=x86_64&target_vers ...
- Angle Beats Gym - 102361A(计算几何)
Angle Beats \[ Time Limit: 4000 ms \quad Memory Limit: 1048576 kB \] 题意 给出 \(n\) 个初始点以及 \(q\) 次询问,每次 ...
- java 栈
package testjavapro; import java.util.*; public class testjava { static void showpush(Stack<Integ ...
- 记忆的轮廓 期望 四边形不等式dp|题解
记忆的轮廓 题目描述 通往贤者之塔的路上,有许多的危机.我们可以把这个地形看做是一颗树,根节点编号为1,目标节点编号为n,其中1-n的简单路径上,编号依次递增,在[1,n]中,一共有n个节点.我们把编 ...
- CentOS中使用FIO测试磁盘IO性能
$ yum install fio 0x02 命令 随机读: $ fio -filename=/dev/sda1 -direct=1 -iodepth 1 -thread -rw=randread - ...
- Docker图形化工具——Portainer
目标搭建docker-ui 一.Docker图形化工具 docker 图形页面管理工具常用的有三种,DockerUI ,Portainer ,Shipyard .DockerUI 是 Portaine ...
- [转帖]美团在Redis上踩过的一些坑-3.redis内存占用飙升
美团在Redis上踩过的一些坑-3.redis内存占用飙升 博客分类: 运维 redis redismonitor内存突增client listinfo 转载请注明出处哈:http://car ...
- 配置opencv cmake
第一种使用 find_package的方法示例代码如下:# 声明要求的 cmake 最低版本cmake_minimum_required( VERSION 2.8 ) # 声明一个 cmake 工程p ...
- MATLAB datenum日期转换为Python日期
摘要 MATLAB datenum时间格式参数众多,本文只简单关注 units 参数,即基准年份和计时度量(天.小时). 命令行演示在 ipython 和 Octave 中进行. 示例1:小时制,基准 ...