前言:
python
的使用者都知道Cpython
解釋器有一個(gè)弊端,真正執(zhí)行時(shí)同一時(shí)間只會(huì)有一個(gè)線程執(zhí)行,這是由于設(shè)計(jì)者當(dāng)初設(shè)計(jì)的一個(gè)缺陷,里面有個(gè)叫GIL鎖
的,但他到底是什么?我們只知道因?yàn)樗麑?dǎo)致python
使用多線程執(zhí)行時(shí),其實(shí)一直是單線程,但是原理卻不知道,那么接下來我們就認(rèn)識(shí)一下GIL鎖
1、什么是GIL鎖
GIL
(Global Interpreter Lock)不是Python
獨(dú)有的特性,它只是在實(shí)現(xiàn)CPython
(Python解釋器)時(shí),引入的一個(gè)概念。
在官方網(wǎng)站中定義如下:
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.)
由定義可知,GIL
是一個(gè)互斥鎖(mutex
)。它阻止了多個(gè)線程同時(shí)執(zhí)行Python
字節(jié)碼,毫無疑問,這降低了執(zhí)行效率。理解GIL的必要性,需要了解CPython
對(duì)于線程安全的內(nèi)存管理機(jī)制。
2、CPython對(duì)線程安全的內(nèi)存管理機(jī)制
Python
使用引用計(jì)數(shù)來進(jìn)行內(nèi)存管理,在Python
中創(chuàng)建的對(duì)象都會(huì)有引用計(jì)數(shù),來記錄有多少個(gè)指針指向它。當(dāng)引用計(jì)數(shù)的值為0時(shí),就會(huì)自動(dòng)釋放內(nèi)存
我們來看一個(gè)小例子,來解釋引用計(jì)數(shù)的原理:
1
2
3
4
5
|
>>> import sys >>> a = [] >>> b = a >>> sys.getrefcount(a) 3 |
可以看到,a
的引用計(jì)數(shù)值為 3
,因?yàn)橛?a
、b
和作為參數(shù)傳遞的 getrefcount
都引用了一個(gè)空列表。
如果有2個(gè)python
線程同時(shí)引用a
,那么2個(gè)線程都會(huì)嘗試對(duì)其進(jìn)行數(shù)據(jù)操作,多個(gè)線程同時(shí)對(duì)一個(gè)數(shù)據(jù)進(jìn)行增加或減少的操作,如果發(fā)生這種情況,則可能導(dǎo)致內(nèi)存泄漏
3、GIL鎖的產(chǎn)生
由于多個(gè)線程同時(shí)對(duì)數(shù)據(jù)進(jìn)行操作,會(huì)引發(fā)數(shù)據(jù)不一致,導(dǎo)致內(nèi)存泄漏,我們可以對(duì)其進(jìn)行加鎖,所以Cpython
就創(chuàng)建了GIL
鎖
但是既然有了鎖,一個(gè)對(duì)象就需要一把鎖,那么多個(gè)對(duì)象就會(huì)有多把鎖,可能會(huì)給我們帶來2個(gè)問題
- 1.死鎖(線程之間互相爭(zhēng)搶鎖的資源)
- 2.反復(fù)獲取和釋放鎖而導(dǎo)致性能降低。
為了保證單線程情況下python
的正常執(zhí)行和效率,GIL鎖
(單一鎖)由此產(chǎn)生了,它添加了一個(gè)規(guī)則,即任何Python
字節(jié)碼的執(zhí)行都需要獲取解釋器鎖。這樣可以防止死鎖(因?yàn)橹挥幸粋€(gè)鎖),并且不會(huì)帶來太多的性能開銷。但這實(shí)際上使所有受CPU
約束的Python
程序(指的是CPU
密集型程序)都是單線程的。
4、GIL鎖的底層原理
上面這張圖,就是 GIL
在 Python
程序的工作示例。其中,Thread 1
、2
、3
輪流執(zhí)行,每一個(gè)線程在開始執(zhí)行時(shí),都會(huì)鎖住 GIL,以阻止別的線程執(zhí)行;同樣的,每一個(gè)線程執(zhí)行完一段后,會(huì)釋放 GIL,以允許別的線程開始利用資源。
線程釋放GIL鎖
有兩種情況:
-
一是遇到
IO操作
, -
二是
Time Tick
到期。IO操作
很好理解
比如發(fā)出一個(gè)http
請(qǐng)求,等待響應(yīng)。那么Time Tick到期是什么呢?Time Tick
規(guī)定了線程的最長(zhǎng)執(zhí)行時(shí)間,超過時(shí)間后自動(dòng)釋放GIL鎖。Python 3
以后,間隔時(shí)間大致為15毫秒
。
雖然都是釋放GIL鎖,但這兩種情況是不一樣的。比如,Thread1遇到IO操作釋放GIL,由Thread2和Thread3來競(jìng)爭(zhēng)這個(gè)GIL鎖,Thread1不再參與這次競(jìng)爭(zhēng)。如果是Thread1因?yàn)門ime Tick到期釋放GIL(多數(shù)是CPU密集型任務(wù)),那么三個(gè)線程可以同時(shí)競(jìng)爭(zhēng)這把GIL鎖,可能出現(xiàn)Thread1
在競(jìng)爭(zhēng)中勝出,再次執(zhí)行的情況。單核CPU下,這種情況不算特別糟糕。因?yàn)橹挥?個(gè)CPU,所以CPU的利用率是很高的。
在多核CPU下,由于GIL鎖的全局特性,無法發(fā)揮多核的特性,GIL鎖會(huì)使得多線程任務(wù)的效率大大降低。
Thread1在CPU1上運(yùn)行,Thread2在CPU2上運(yùn)行。GIL是全局的,CPU2上的Thread2需要等待CPU1上的Thread1讓出GIL鎖,才有可能執(zhí)行。如果在多次競(jìng)爭(zhēng)中,Thread1都勝出,Thread2沒有得到GIL鎖,意味著CPU2一直是閑置的,無法發(fā)揮多核的優(yōu)勢(shì)。
為了避免同一線程霸占CPU,在python3.2
版本之后,線程會(huì)自動(dòng)的調(diào)整自己的優(yōu)先級(jí),使得多線程任務(wù)執(zhí)行效率更高。
既然GIL降低了多核的效率,那保留它的目的是什么呢?這就和線程執(zhí)行的安全有關(guān)。
5、Python GIL不能絕對(duì)保證線程安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
def add(): global n for i in range( 10 ** 1000 ): n = n + 1 def sub (): global n for i in range( 10 ** 1000 ): n = n - 1 n = 0 import threading a = threading.Thread(target=add,) b = threading.Thread(target= sub ,) a.start() b.start() a.join() b.join() print n |
上面的程序?qū)做了同樣數(shù)量的加法和減法,那么n理論上是0。但運(yùn)行程序,打印n,發(fā)現(xiàn)它不是0。問題出在哪里呢,問題在于python的每行代碼不是原子化的操作。比如n = n+1
這步,不是一次性執(zhí)行的。如果去查看python編譯后的字節(jié)碼執(zhí)行過程,可以看到如下結(jié)果。
1
2
3
4
|
19 LOAD_GLOBAL 1 (n) 22 LOAD_CONST 3 (1) 25 BINARY_ADD 26 STORE_GLOBAL 1 (n) |
從過程可以看出,n = n +1
操作分成了四步完成。因此,n = n+1
不是一個(gè)原子化操作。
- 1.加載全局變量n
- 2.加載常數(shù)1
- 3.進(jìn)行二進(jìn)制加法運(yùn)算
- 4.將運(yùn)算結(jié)果存入變量n。
根據(jù)前面的線程釋放GIL鎖
原則,線程a執(zhí)行這四步的過程中,有可能會(huì)讓出GIL
。如果這樣,n=n+1
的運(yùn)算過程就被打亂了。最后的結(jié)果中,得到一個(gè)非零的n也就不足為奇。
6、總結(jié)
對(duì)于IO密集型應(yīng)用,多線程的應(yīng)用和多進(jìn)程應(yīng)用區(qū)別不大。即便有GIL
存在,由于IO操作
會(huì)導(dǎo)致GIL釋放,其他線程能夠獲得執(zhí)行權(quán)限。由于多線程的通訊成本低于多進(jìn)程,因此偏向使用多線程。
對(duì)于計(jì)算密集型應(yīng)用,由于CPU一直處于被占用狀態(tài),GIL鎖直到規(guī)定時(shí)間才會(huì)釋放,然后才會(huì)切換狀態(tài),導(dǎo)致多線程處于絕對(duì)的劣勢(shì),此時(shí)可以采用多進(jìn)程+協(xié)程。
到此這篇關(guān)于python 深入了解GIL鎖詳細(xì)的文章就介紹到這了,更多相關(guān)python 深入了解GIL鎖內(nèi)容請(qǐng)搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
參考資料:
https://realpython.com/python-gil/
https://zhuanlan.zhihu.com/p/97218985
原文鏈接:https://www.cnblogs.com/jiakecong/p/14693491.html