深入理解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.pswp.cn/news/532590.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/532590.shtml
英文地址,請注明出處:http://en.pswp.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編碼了。 登錄后頁面:…

Libtorch報錯:terminate called after throwing an instance of ‘c10::Error‘ what():isTensor()INTERNAL ASS

Libtorch報錯:terminate called after throwing an instance of ‘c10::Error’ what(): isTensor() INTERNAL ASSERT FAILED 報錯 問題出現在筆者想要將 yolov5 通過 PyTorch 的 C 接口 Libtorch 部署到樹莓派上。 完整報錯信息: terminate called …

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

知識點: 1.堆疊注入原理(stacked injection) 在SQL中,分號(;)是用來表示一條sql語句的結束。試想一下我們在 ; 結束一個sql語句后繼續構造下一條語句,會不會一起執行?因此這個想法…

mysql常規使用(建立,增刪改查,視圖索引)

目錄 1.數據庫建立 2.增刪改查 3.視圖建立: 1.數據庫建立 mysql> mysql> show databases; ----------------------------------- | Database | ----------------------------------- | information_schema | | ch…

php操作mysql數據庫

phpmyadmin phpadmin是一個mysql圖形化管理工具,是一款實用php開發的mysql苦戶端軟件,基于web跨平臺的管理系統,支持簡體中文,官網:www.phpmyadmin.net可以下載免費最新版。提供圖形化操作界面,完成對mysq…

C:C++ 函數返回多個參數

C/C 函數返回多個參數 轉自:https://blog.csdn.net/onlyou2030/article/details/48174461 筆者是 Python 入門的,一直很困惑 C/C 中函數如何返回多個參數。 如果一個函數需要返回多個參數,可以采用以下兩種方法: 傳引用或指針…

sql預編譯

一.數據庫預編譯起源: 數據庫接受sql語句,需要解析和制定執行,中間需要花費一段時間. 有時候同一語句可能會多次執行, 那么就會造成資源的浪費 如何減少編譯執行的時間 ? 就有了預編譯,預編譯是將這類語句提前用占位符替代,一次編譯,多次執行. 預編譯后的執行代碼會被緩存下來…

C++中智能指針的原理、使用、實現

C中智能指針的原理、使用、實現 轉自:https://www.cnblogs.com/wxquare/p/4759020.html 1 智能指針的作用 C程序設計中使用堆內存是非常頻繁的操作,堆內存的申請和釋放都由程序員自己管理。程序員自己管理堆內存可以提高了程序的效率,但是…

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是一個開源的內容管理框架,…

C++中類的拷貝控制

C中類的拷貝控制 轉自:https://www.cnblogs.com/ronny/p/3734110.html 1,什么是類的拷貝控制 當我們定義一個類的時候,為了讓我們定義的類類型像內置類型(char,int,double等)一樣好用,我們通常需要考下面…

centos7ubuntu搭建Vulhub靶場(推薦Ubuntu)

這里寫目錄標題一.前言總結二.成功操作:三.出現報錯:四.vulhub使用正文:一.前言總結二.成功操作:三.出現報錯:四.vulhub使用看完點贊關注不迷路!!!! 后續繼續更新優質安全內容!!!!!一.前言總結 二.成功操作&#xff1…

使用 PyTorch 數據讀取,JAX 框架來訓練一個簡單的神經網絡

使用 PyTorch 數據讀取,JAX 框架來訓練一個簡單的神經網絡 本文例程部分主要參考官方文檔。 JAX簡介 JAX 的前身是 Autograd ,也就是說 JAX 是 Autograd 升級版本,JAX 可以對 Python 和 NumPy 程序進行自動微分。可以通過 Python的大量特征…

Yapi Mock 遠程代碼執行漏洞

跟風一波復現Yapi 漏洞描述: YApi接口管理平臺遠程代碼執行0day漏洞,攻擊者可通過平臺注冊用戶添加接口,設置mock腳本從而執行任意代碼。鑒于該漏洞目前處于0day漏洞利用狀態,強烈建議客戶盡快采取緩解措施以避免受此漏洞影響 …

C++ ACM模式輸入輸出

C ACM模式輸入輸出 以下我們都以求和作為題目要求,來看一下各種輸入輸出應該怎么寫。 1 只有一個或幾個輸入 輸入樣例: 3 5 7輸入輸出模板: int main() {int a, b, c;// 接收有限個輸入cin >> a >> b >> c;// 輸出結果…

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

漏洞產生原因: CVE-2017-10271漏洞產生的原因大致是Weblogic的WLS Security組件對外提供webservice服務,其中使用了XMLDecoder來解析用戶傳入的XML數據,在解析的過程中出現反序列化漏洞,導致可執行任意命令。攻擊者發送精心構造的…

樹莓派攝像頭 C++ OpenCV YoloV3 實現實時目標檢測

樹莓派攝像頭 C OpenCV YoloV3 實現實時目標檢測 本文將實現樹莓派攝像頭 C OpenCV YoloV3 實現實時目標檢測,我們會先實現樹莓派對視頻文件的逐幀檢測來驗證算法流程,成功后,再接入攝像頭進行實時目標檢測。 先聲明一下筆者的主要軟硬件配…