深入理解Python中的全局解释锁GIL

深入理解Python中的全局解释锁GIL

转自:https://zhuanlan.zhihu.com/p/75780308

:本文为蜗牛学院资深讲师卿淳俊老师原创,首发自公众号https://mp.weixin.qq.com/s/TBiqbSCsjIbNIk8ATky-tg,如需转载请私聊我处获得授权并注明出处。

Python是门古老的语言,要想了解这门语言的多线程和多进程以及协程,以及明白什么时候应该用多线程,什么时候应该使用多进程或协程,我们不得不谈到的一个东西是Python中的GIL(全局解释器锁)。这篇我们就来看看这个GIL究竟是怎么回事。

1. GIL是什么?

首先来看看GIL究竟是什么。我们需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。

那么CPython实现中的GIL又是什么呢?GIL全称Global Interpreter Lock为了避免误导,我们还是来看一下官方给出的解释:

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

这看起来像一个Bug一般的防止多线程并发执行机器码的互斥锁(mutex),究竟为什么会存在?

2. GIL为什么会存在?

GIL的问题其实是由于近十几年来应用程序和操作系统逐步从多任务单核心演进到多任务多核心导致的 , 在一个古老的单核CPU上调度多个线程任务,大家相互共享一个全局锁,谁在CPU执行,谁就占有这把锁,直到这个线程因为IO操作或者Timer Tick到期让出CPU,没有在执行的线程就安静的等待着这把锁(除了等待之外,他们应该也无事可做)。下面这个图演示了一个单核CPU的线程调度方式:

在这里插入图片描述

很明显,在一个现代多核心的处理器上,上面的模型就有很大优化空间了,原来只能等待的线程任务,现在可以在其它空闲的核心上调度并发执行。由于古老GIL机制,如果线程2需要在CPU 2 上执行,它需要先等待在CPU 1 上执行的线程1释放GIL(记住:GIL是全局的)。如果线程1是因为 i/o 阻塞让出的GIL,那么线程2必定拿到Gil。但如果线程1是因为timer ticks计数满100让出GIL,那么这个时候线程1和线程2公平竞争。但要命的是,在Python 2.x, 线程1不会动态的调整自身的优先级,所以很大概率下次被选中执行的还是线程1,在很多个这样的选举周期内,线程2只能安静的看着线程1拿着GIL在CPU 1上欢快的执行。

在这里插入图片描述

在稍微极端一点的情况下,比如线程1使用了while True在CPU 1 上执行,那就真是“一核有难,八核围观”了,如下图所示:

在这里插入图片描述

接下来,我们用实际代码来看看GIL的存在对多线程运行的影响(此处使用循环模拟耗时操作):

# coding:utf-8
import threading, timedef my_counter():i = 0for _ in range(100000000):i = i+1return Truedef main1():thread_ary = {}start_time = time.time()for tid in range(2):t = threading.Thread(target=my_counter)t.start()t.join()  # 第一次循环的时候join方法引起主线程阻塞,但第二个线程并没有启动,所以两个线程是顺序执行的print("单线程顺序执行total_time: {}".format(time.time() - start_time))def main2():thread_ary = {}start_time = time.time()for tid in range(2):t = threading.Thread(target=my_counter)t.start()thread_ary[tid] = tfor i in range(2):thread_ary[i].join()  # 两个线程均已启动,所以两个线程是并发的print("并发执行total_time: {}".format(time.time() - start_time))if __name__ == "__main__":main1()main2()

上面这段代码,在Python3上运行,不管是并发执行还是顺序执行,运行时间都差不多,这充分说明了GIL确实会在这种情况下对多线程程序的运行效率产生影响。如果是在Python2上运行,则差距更明显。

在这里插入图片描述

Python3.6上运行的结果

在这里插入图片描述

Python2.7上运行的结果

(Python3.2以后对GIL做了较大的优化)

3. GIL是否意味着线程安全

有GIL并不意味着python一定是线程安全的,那什么时候安全,什么时候不安全,我们必须搞清楚。之前我们已经说过,一个线程有两种情况下会释放全局解释器锁,一种情况是在该线程进入IO操作之前,会主动释放GIL,另一种情况是解释器不间断运行了1000字节码(Py2)或运行15毫秒(Py3)后,该线程也会放弃GIL。既然一个线程可能随时会失去GIL,那么这就一定会涉及到线程安全的问题。GIL虽然从设计的出发点就是考虑到线程安全,但这种线程安全是粗粒度的线程安全,即不需要程序员自己对线程进行加锁处理(同理,所谓细粒度就是指程序员需要自行加、解锁来保证线程安全,典型代表是 Java , 而 CPthon 中是粗粒度的锁,即语言层面本身维护着一个全局的锁机制,用来保证线程安全)。那么什么时候需要加锁,什么时候不需要加锁,这个需要具体情况具体分析。下面我们就来针对每种可能的情况进行分析和总结。

首先来看第一种线程释放GIL的情况。假设现在线程A因为进入IO操作而主动释放了GIL,那么在这种情况下,由于线程A的IO操作等待时间不确定,那么等待的线程B一定会得到GIL锁,这种比较“礼貌的”情况我们一般称为“协同式多任务处理”,相当于大家按照协商好的规则来,线程是安全的,不需要额外加锁。

接下来,我们来看另外一种情况,即线程A是因为解释器不间断执行了1000字节码的指令或不间断运行了15毫秒而放弃了GIL,那么此时实际上线程A和线程B将同时竞争GIL锁。在同时竞争的情况下,实际上谁会竞争成功是不确定的一个结果,所以一般被称为“抢占式多任务处理”,这种情况下当然就看谁抢得厉害了。当然,在python3上由于对GIL做了优化,并且会动态调整线程的优先级,所以线程B的优先级会比较高,但仍然无法肯定线程B就一定会拿到GIL。那么在这种情况下,线程可能就会出现不安全的状态。针对这种纯计算的操作,我们用一段代码来演示下这种线程不安全的状态。代码如下:

import threadingn = 0def add():global nfor i in range(1000000):n = n + 1def sub():global nfor i in range(1000000):n = n - 1if __name__ == "__main__":t1 = threading.Thread(target=add)t2 = threading.Thread(target=sub)t1.start()t2.start()t1.join()t2.join()print('n =', n)

上面的代码很简单,分别用线程1和线程2对全局变量n进行了1000000次的加和减操作。如果线程安全的话,那么最终的结果n应该还是为0。但实际上,我们运行之后,会发现这个n的值有时大有时小,完全不确定。这就是典型的多个线程操作同一个全局变量造成的线程不安全的问题。我们明白了这个问题,在这里我们只讨论产生问题的原因,在后面的文章中再来讨论如何通过加锁来解决这个问题。

接下来,我们从代码层面分析下产生这个问题的原因。在线程中,我们主要是执行了一个加法和减法的操作。为了方便说明问题,我们把函数最简化到一个加法函数和一个减法函数,来分析它们的字节码执行过程,来看看释放GIL锁是怎么引起这个问题的。演示代码如下:

import dis
n = 0def add():global nn = n + 1print(dis.dis(add))def sub():global nn = n - 1
print(dis.dis(sub))

dis模块中的dis方法可以打印出一个函数对应的字节码执行过程,所以非常方便我们进行分析。运行结果如下:

在这里插入图片描述

不管是加法还是减法运算,都会分为4步完成。以加法为例,第一步是LOAD_GLOBAL(加载全局变量n),第二步LOAD_CONST(加载常量1),第三步进行二进制的加法,第四步将计算结果存储到全局变量n中,加法计算结束。这四个指令如果能够保证被作为一个整体完整地运行,那么是不会产生问题的,但根据前面说的线程释放GIL的原则,那么很有可能在线程正在执行这四步中的任何一步的时候释放掉GIL而进入等待状态,这个时候发生的事情就比较有意思了。为了方便大家理解,我拿一种比较极端的情况来说明一下。比如我们在加法运算中,正准备执行第四步的时候,很不幸失去了GIL,进入等待状态(注意此时n值仍然为0)。减法运算的线程开始执行,它加载了全局变量n(值为0),并进行减法相关的计算,它也在执行第三步的时候失去了GIL,此时它进入等待状态,加法运算继续。上一次加法计算继续运行第4步,即把加法运算结果赋值给全局变量n,那么此时n的值为1。同样道理,减法操作拿回GIL时,它之前已经加载了为0的n的值,所以它继续操作到最后赋值那步时,n的值就为0-1=-1。换句话说,n的值要么为1,要么为-1,但我们期望的应该是0。这就造成了线程不安全的情形。最终,经过百万次这样不确定的加减操作,那么结果一定是不确定的。这就是引起这个问题的过程和原因。

接下来,我们还要解决另外一个问题,也就是既然GIL从粗粒度情况下存在线程不安全的可能性,那么是不是所有非IO操作引起的GIL释放都要加锁来解决线程安全的问题。这个问题同样要分情况,因为python跟其他线程自由的语言比如 Java相比,它有很多操作是原子级的,针对原子级的操作,由于方法本身是单个字节码,所以线程没有办法在调用期间放弃GIL。典型的例子比如sort方法,我们同样可以看看这种原子级的操作在python的字节码中是什么样子,代码演示如下:

import dislst = [4, 1, 3, 2]def foo():lst.sort()print(dis.dis(foo))

运行后结果如下:

在这里插入图片描述

从字节码的角度,调用sort操作是原子级无法再分的,所以线程不会在执行期间发生GIL释放的情况,也就是说我们可以认为sort操作是线程安全的,不需要加锁。而我们上面演示的加法和减法操作则不是原子级的,所以我们必须要加锁才能保证线程安全。

所以,总结一下,如果多线程的操作中不是IO密集型,并且计算操作不是原子级的操作时,那么我们需要考虑线程安全问题,否则都不需要考虑线程安全。当然,为了避免担心哪个操作是原子的,我们可以遵循一个简单的原则:始终围绕共享可变状态的读取和写入加锁。毕竟,在 Python 中获取一个 threading.Lock 也就是一行代码的事。

4. 如何避免GIL的影响

有两个建议:

  1. 在以IO操作为主的IO密集型应用中,多线程和多进程的性能区别并不大,原因在于即使在Python中有GIL锁的存在,由于线程中的IO操作会使得线程立即释放GIL,切换到其他非IO线程继续操作,提高程序执行效率。相比进程操作,线程操作更加轻量级,线程之间的通讯复杂度更低,建议使用多线程。

  2. 如果是计算密集型的应用,尽量使用多进程或者协程来代替多线程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/532590.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

sqli-lab————Writeup(18~20)各种头部注入

less18 基于错误的用户代理,头部POST注入 admin admin 登入成功(进不去重置数据库) 显示如下 有user agent参数,可能存在注入点 显示版本号: 爆库:User-Agent:and extractvalue(1,concat(0x7e,(select …

Python GIL

转自:https://blog.csdn.net/weixin_41594007/article/details/79485847 Python GIL 在进行GIL讲解之前,我们可以先回顾一下并行和并发的区别: 并行:多个CPU同时执行多个任务,就好像有两个程序,这两个程序…

sqli-lab——Writeup21~38(各种过滤绕过WAF和)

Less-21 Cookie Injection- Error Based- complex - string ( 基于错误的复杂的字符型Cookie注入) base64编码,单引号,报错型,cookie型注入。 本关和less-20相似,只是cookie的uname值经过base64编码了。 登录后页面:…

sqli-lab——Writeup(38~over)堆叠等......

知识点: 1.堆叠注入原理(stacked injection) 在SQL中,分号(;)是用来表示一条sql语句的结束。试想一下我们在 ; 结束一个sql语句后继续构造下一条语句,会不会一起执行?因此这个想法…

mysql常规使用(建立,增删改查,视图索引)

目录 1.数据库建立 2.增删改查 3.视图建立: 1.数据库建立 mysql> mysql> show databases; ----------------------------------- | Database | ----------------------------------- | information_schema | | ch…

Xctf练习sql注入--supersqli

三种方法 方法一 1 回显正常 1’回显不正常,报sql语法错误 1’ -- 回显正常,说明有sql注入点,应该是字符型注入(# 不能用) 1’ order by 3 -- 回显失败,说明有2个注入点 1’ union select 1,2 -- 回显显示过滤语句: 1’; show databases -- 爆数据库名 -1’; show tables …

深拷贝与浅拷贝、值语义与引用语义对象语义 ——以C++和Python为例

深拷贝与浅拷贝、值语义与引用语义/对象语义 ——以C和Python为例 值语义与引用语义(对象语义) 本小节参考自:https://www.cnblogs.com/Solstice/archive/2011/08/16/2141515.html 概念 在任何编程语言中,区分深浅拷贝的关键都…

一次打卡软件的实战渗透测试

直接打卡抓包, 发现有疑似企业网站,查ip直接显示以下页面 直接显示了后台安装界面…就很有意思 探针和phpinfo存在 尝试连接mysql失败 fofa扫描为阿里云服务器 找到公司官网使用nmap扫描,存在端口使用onethink 查询onethink OneThink是一个开源的内容管理框架,…

centos7ubuntu搭建Vulhub靶场(推荐Ubuntu)

这里写目录标题一.前言总结二.成功操作:三.出现报错:四.vulhub使用正文:一.前言总结二.成功操作:三.出现报错:四.vulhub使用看完点赞关注不迷路!!!! 后续继续更新优质安全内容!!!!!一.前言总结 二.成功操作&#xff1…

Yapi Mock 远程代码执行漏洞

跟风一波复现Yapi 漏洞描述: YApi接口管理平台远程代码执行0day漏洞,攻击者可通过平台注册用户添加接口,设置mock脚本从而执行任意代码。鉴于该漏洞目前处于0day漏洞利用状态,强烈建议客户尽快采取缓解措施以避免受此漏洞影响 …

CVE-2017-10271 WebLogic XMLDecoder反序列化漏洞

漏洞产生原因: CVE-2017-10271漏洞产生的原因大致是Weblogic的WLS Security组件对外提供webservice服务,其中使用了XMLDecoder来解析用户传入的XML数据,在解析的过程中出现反序列化漏洞,导致可执行任意命令。攻击者发送精心构造的…

树莓派摄像头 C++ OpenCV YoloV3 实现实时目标检测

树莓派摄像头 C OpenCV YoloV3 实现实时目标检测 本文将实现树莓派摄像头 C OpenCV YoloV3 实现实时目标检测,我们会先实现树莓派对视频文件的逐帧检测来验证算法流程,成功后,再接入摄像头进行实时目标检测。 先声明一下笔者的主要软硬件配…

【实战】记录一次服务器挖矿病毒处理

信息收集及kill: 查看监控显示长期CPU利用率超高,怀疑中了病毒 top 命令查看进程资源占用: netstat -lntupa 命令查看有无ip进行发包 netstat -antp 然而并没有找到对应的进程名 查看java进程和solr进程 ps aux :查看所有进程…

ag 搜索工具参数详解

ag 搜索工具参数详解 Ag 是类似ack, grep的工具,它来在文件中搜索相应关键字。 官方列出了几点选择它的理由: 它比ack还要快 (和grep不在一个数量级上)它会忽略.gitignore和.hgignore中的匹配文件如果有你想忽略的文…

CVE-2013-4547 文件名逻辑漏洞

搭建环境,访问 8080 端口 漏洞说明: Nginx: Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,在BSD-like 协议下发行。其特点是占有内存少,并发能力强&#xf…

CVE-2017-7529Nginx越界读取缓存漏洞POC

漏洞影响 低危,造成信息泄露,暴露真实ip等 实验内容 漏洞原理 通过查看patch确定问题是由于对http header中range域处理不当造成,焦点在ngx_http_range_parse 函数中的循环: HTTP头部range域的内容大约为Range: bytes4096-81…

Linux命令行性能监控工具大全

Linux命令行性能监控工具大全 作者:Arnold Lu 原文:https://www.cnblogs.com/arnoldlu/p/9462221.html 关键词:top、perf、sar、ksar、mpstat、uptime、vmstat、pidstat、time、cpustat、munin、htop、glances、atop、nmon、pcp-gui、collect…

Weblogic12c T3 协议安全漏洞分析【CVE-2020-14645 CVE-2020-2883 CVE-2020-14645】

给个关注?宝儿! 给个关注?宝儿! 给个关注?宝儿! 关注公众号:b1gpig信息安全,文章推送不错过 ## 前言 WebLogic是美国Oracle公司出品的一个application server,确切的说是一个基于JAV…

Getshell总结

按方式分类: 0x01注入getshell: 0x02 上传 getwebshell 0x03 RCE getshell 0x04 包含getwebshell 0x05 漏洞组合拳getshell 0x06 系统层getcmdshell 0x07 钓鱼 getcmdshell 0x08 cms后台getshell 0x09 红队shell竞争分析 0x01注入getshell:…

python 到 poc

0x01 特殊函数 0x02 模块 0x03 小工具开发记录 特殊函数 # -*- coding:utf-8 -*- #内容见POC.demo; POC.demo2 ;def add(x,y):axyprint(a)add(3,5) print(------------引入lambad版本:) add lambda x,y : xy print(add(3,5)) #lambda函数,在lambda函数后面直接…