搜尋
首頁後端開發Python教學Python中的GIL是什麼

Python中的GIL是什麼

May 14, 2023 pm 02:40 PM
pythongil

為什麼需要 GIL

GIL 本質上是一把鎖,學過作業系統的同學都知道鎖的引入是為了避免並發存取造成資料的不一致。 CPython 中有很多定義在函數外面的全域變量,例如記憶體管理中的 usable_arenas 和 usedpools,如果多個執行緒同時申請記憶體就可能同時修改這些變量,造成資料錯亂。另外Python 的垃圾回收機制是基於引用計數的,所有對像都有一個 ob_refcnt字段表示當前有多少變量會引用當前對象,變量賦值、參數傳遞等操作都會增加引用計數,退出作用域或函數返回會減少引用計數。同樣地,如果有多個執行緒同時修改同一個物件的引用計數,就有可能使 ob_refcnt 與真實值不同,可能會造成記憶體洩漏,不會被使用的物件無法回收,更嚴重可能會回收還在被引用的對象,造成Python 解釋器崩潰。

GIL 的實作

CPython 中GIL 的定義如下

struct _gil_runtime_state {
    unsigned long interval; // 请求 GIL 的线程在 interval 毫秒后还没成功,就会向持有 GIL 的线程发出释放信号
    _Py_atomic_address last_holder; // GIL 上一次的持有线程,强制切换线程时会用到
    _Py_atomic_int locked; // GIL 是否被某个线程持有
    unsigned long switch_number; // GIL 的持有线程切换了多少次
    // 条件变量和互斥锁,一般都是成对出现
    PyCOND_T cond;
    PyMUTEX_T mutex;
    // 条件变量,用于强制切换线程
    PyCOND_T switch_cond;
    PyMUTEX_T switch_mutex;
};

最本質的是mutex 保護的locked 字段,表示GIL 目前是否被持有,其他字段是為了優化GIL 而被用到的。執行緒申請 GIL 時會呼叫 take_gil() 方法,釋放 GIL時 呼叫 drop_gil() 方法。為了避免飢餓現象,當一個執行緒等待了interval 毫秒(預設是5 毫秒)還沒申請到GIL 的時候,就會主動向持有GIL 的執行緒發出訊號,GIL 的持有者會在恰當時機檢查該訊號,如果發現有其他線程在申請就會強制釋放GIL。這裡所說的恰當時機在不同版本中有所不同,早期是每執行100 條指令會檢查一次,在Python 3.10.4 中是在條件語句結束、循環語句的每次循環體結束以及函數調用結束的時候才會去檢查。

申請 GIL 的函數 take_gil() 簡化後如下

static void take_gil(PyThreadState *tstate)
{
    ...
    // 申请互斥锁
    MUTEX_LOCK(gil->mutex);
    // 如果 GIL 空闲就直接获取
    if (!_Py_atomic_load_relaxed(&gil->locked)) {
        goto _ready;
    }
    // 尝试等待
    while (_Py_atomic_load_relaxed(&gil->locked)) {
        unsigned long saved_switchnum = gil->switch_number;
        unsigned long interval = (gil->interval >= 1 ? gil->interval : 1);
        int timed_out = 0;
        COND_TIMED_WAIT(gil->cond, gil->mutex, interval, timed_out);
        if (timed_out &&  _Py_atomic_load_relaxed(&gil->locked) && gil->switch_number == saved_switchnum) {
            SET_GIL_DROP_REQUEST(interp);
        }
    }
_ready:
    MUTEX_LOCK(gil->switch_mutex);
    _Py_atomic_store_relaxed(&gil->locked, 1);
    _Py_ANNOTATE_RWLOCK_ACQUIRED(&gil->locked, /*is_write=*/1);

    if (tstate != (PyThreadState*)_Py_atomic_load_relaxed(&gil->last_holder)) {
        _Py_atomic_store_relaxed(&gil->last_holder, (uintptr_t)tstate);
        ++gil->switch_number;
    }
    // 唤醒强制切换的线程主动等待的条件变量
    COND_SIGNAL(gil->switch_cond);
    MUTEX_UNLOCK(gil->switch_mutex);
    if (_Py_atomic_load_relaxed(&ceval2->gil_drop_request)) {
        RESET_GIL_DROP_REQUEST(interp);
    }
    else {
        COMPUTE_EVAL_BREAKER(interp, ceval, ceval2);
    }
    ...
    // 释放互斥锁
    MUTEX_UNLOCK(gil->mutex);
}

整個函數體為了確保原子性,需要在開頭和結尾分別申請和釋放互斥鎖 gil->mutex。如果當前GIL 是空閒狀態就直接取得GIL,如果不空閒就等待條件變數 gil->cond interval 毫秒(不小於1 毫秒),如果逾時且期間沒有發生過GIL 切換就將 gil_drop_request 置位,請求強制切換GIL 持有線程,否則繼續等待。一旦取得 GIL 成功需要更新 gil->locked、gil->last_holder 和 gil->switch_number 的值,喚醒條件變數 gil->switch_cond,並且釋放互斥鎖 gil->mutex。

釋放 GIL 的函數 drop_gil() 簡化後如下

static void drop_gil(struct _ceval_runtime_state *ceval, struct _ceval_state *ceval2,
         PyThreadState *tstate)
{
    ...
    if (tstate != NULL) {
        _Py_atomic_store_relaxed(&gil->last_holder, (uintptr_t)tstate);
    }
    MUTEX_LOCK(gil->mutex);
    _Py_ANNOTATE_RWLOCK_RELEASED(&gil->locked, /*is_write=*/1);
    // 释放 GIL
    _Py_atomic_store_relaxed(&gil->locked, 0);
    // 唤醒正在等待 GIL 的线程
    COND_SIGNAL(gil->cond);
    MUTEX_UNLOCK(gil->mutex);
    if (_Py_atomic_load_relaxed(&ceval2->gil_drop_request) && tstate != NULL) {
        MUTEX_LOCK(gil->switch_mutex);
        // 强制等待一次线程切换才被唤醒,避免饥饿
        if (((PyThreadState*)_Py_atomic_load_relaxed(&gil->last_holder)) == tstate)
        {
            assert(is_tstate_valid(tstate));
            RESET_GIL_DROP_REQUEST(tstate->interp);
            COND_WAIT(gil->switch_cond, gil->switch_mutex);
        }
        MUTEX_UNLOCK(gil->switch_mutex);
    }
}

首先在 gil->mutex 的保護下釋放 GIL,然後喚醒其他正在等待 GIL 的執行緒。在多CP​​U 的環境下,當前線程在釋放GIL 後有更高的概率重新獲得GIL,為了避免對其他線程造成飢餓,當前線程需要強制等待條件變量 gil->switch_cond,只有在其他線程獲取GIL 的時候當前執行緒才會被喚醒。

幾點說明

GIL 最佳化

受GIL 約束的程式碼不能並行執行,降低了整體效能,為了盡量降低效能損失,Python 在進行IO 操作或不涉及物件存取的密集CPU 運算的時候,會主動釋放GIL,減少了GIL 的粒度,例如

  • 讀寫檔案

  • 網路存取

  • 加密資料/壓縮資料

#所以嚴格來說,在單一進程的情況下,多個Python 執行緒時可能同時執行的,例如一個執行緒在正常運行,另一個執行緒在壓縮資料。

使用者資料的一致性不能依賴 GIL

GIL 是為了維護 Python 解釋器內部變數的一致性而產生的鎖,使用者資料的一致性不由 GIL 負責。雖然GIL 在某種程度上也保證了使用者資料的一致性,例如Python 3.10.4 中不涉及跳躍和函數呼叫的指令都會在GIL 的約束下原子性的執行,但是資料在業務邏輯上的一致性需要用戶自己加鎖來保證。

下面的程式碼用兩個線程模擬用戶集碎片得獎

from threading import Thread

def main():
    stat = {"piece_count": 0, "reward_count": 0}
    t1 = Thread(target=process_piece, args=(stat,))
    t2 = Thread(target=process_piece, args=(stat,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(stat)

def process_piece(stat):
    for i in range(10000000):
        if stat["piece_count"] % 10 == 0:
            reward = True
        else:
            reward = False
        if reward:
            stat["reward_count"] += 1
        stat["piece_count"] += 1

if __name__ == "__main__":
    main()

假設用戶每集齊10 個碎片就能得到一次獎勵,每個線程收集了10000000 個碎片,應該得到9999999 個獎勵(最後一次沒有計算),總共應該收集20000000 個碎片,得到1999998 個獎勵,但是在我電腦上一次運行結果如下

{'piece_count': 20000000, 'reward_count': 1999987}

總的碎片數量與預期一致,但是獎勵數量卻少了12 個。碎片數量正確是因為在 Python 3.10.4 中,stat["piece_count"] = 1 是在 GIL 約束下原子性執行的。由於每次循環結束都可能切換執行線程,那麼可能線程t1 在某次循環結束時將 piece_count 加到100,但是在下次循環開始模10 判斷前,Python 解釋器切換到線程t2 執行,t2 將 piece_count 加到101,那麼就會錯過一次獎勵。

附:如何避免受到GIL的影響

說了那麼多,如果不說解決方案就只是個科普帖,然並卵。 GIL這麼爛,有沒有辦法繞過呢?讓我們來看看有哪些現成的方案。

用multiprocess取代Thread

multiprocess函式庫的出現很大程度上是為了彌補thread函式庫因為GIL而低效的缺陷。它完整​​的複製了一套thread所提供的介面方便遷移。唯一的不同就是它使用了多進程而不是多執行緒。每個進程都有自己的獨立的GIL,因此也不會出現進程之間的GIL爭搶。

當然multiprocess也不是萬用良藥。它的引入會增加程式實現時線程間資料通訊和同步的困難。就拿計數器來舉例子,如果我們要多個線程累加同一個變量,對於thread來說,申明一個global變量,用thread.Lock的context包裹住三行就搞定了。而multiprocess由於進程之間無法看到對方的數據,只能透過在主執行緒申明一個Queue,put再get或用share memory的方法。這個額外的實現成本使得原本就非常痛苦的多執行緒程式編碼,變得更加痛苦了。具體難點在哪有興趣的讀者可以擴充閱讀這篇文章

用其他解析器

#之前也提到了既然GIL只是CPython的產物,那麼其他解析器是不是更好呢?沒錯,像JPython和IronPython這樣的解析器由於實作語言的特性,他們不需要GIL的幫助。然而由於用了Java/C#用於解析器實現,他們也失去了利用社群眾多C語言模組有用特性的機會。所以這些解析器也因此一直都比較小眾。畢竟功能和效能大家在初期都會選擇前者,Done is better than perfect。

所以沒救了麼?

當然Python社群也在非常努力的不斷改進GIL,甚至是嘗試去除GIL。並在各個小版本中有了不少的進步。有興趣的讀者可以擴展閱讀這個Slide

另一個改進Reworking the GIL

#– 將切換顆粒度從基於opcode計數改成基於時間片計數

#&ndash ; 避免最近一次釋放GIL鎖定的執行緒再次被立即調度

– 新增執行緒優先權功能(高優先權執行緒可以迫使其他執行緒釋放所持有的GIL鎖定)

以上是Python中的GIL是什麼的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:亿速云。如有侵權,請聯絡admin@php.cn刪除
Python的科學計算中如何使用陣列?Python的科學計算中如何使用陣列?Apr 25, 2025 am 12:28 AM

Arraysinpython,尤其是Vianumpy,ArecrucialInsCientificComputingfortheireftheireffertheireffertheirefferthe.1)Heasuedfornumerericalicerationalation,dataAnalysis和Machinelearning.2)Numpy'Simpy'Simpy'simplementIncressionSressirestrionsfasteroperoperoperationspasterationspasterationspasterationspasterationspasterationsthanpythonlists.3)inthanypythonlists.3)andAreseNableAblequick

您如何處理同一系統上的不同Python版本?您如何處理同一系統上的不同Python版本?Apr 25, 2025 am 12:24 AM

你可以通過使用pyenv、venv和Anaconda來管理不同的Python版本。 1)使用pyenv管理多個Python版本:安裝pyenv,設置全局和本地版本。 2)使用venv創建虛擬環境以隔離項目依賴。 3)使用Anaconda管理數據科學項目中的Python版本。 4)保留系統Python用於系統級任務。通過這些工具和策略,你可以有效地管理不同版本的Python,確保項目順利運行。

與標準Python陣列相比,使用Numpy數組的一些優點是什麼?與標準Python陣列相比,使用Numpy數組的一些優點是什麼?Apr 25, 2025 am 12:21 AM

numpyarrayshaveseveraladagesoverandastardandpythonarrays:1)基於基於duetoc的iMplation,2)2)他們的aremoremoremorymorymoremorymoremorymoremorymoremoremory,尤其是WithlargedAtasets和3)效率化,效率化,矢量化函數函數函數函數構成和穩定性構成和穩定性的操作,製造

陣列的同質性質如何影響性能?陣列的同質性質如何影響性能?Apr 25, 2025 am 12:13 AM

數組的同質性對性能的影響是雙重的:1)同質性允許編譯器優化內存訪問,提高性能;2)但限制了類型多樣性,可能導致效率低下。總之,選擇合適的數據結構至關重要。

編寫可執行python腳本的最佳實踐是什麼?編寫可執行python腳本的最佳實踐是什麼?Apr 25, 2025 am 12:11 AM

到CraftCraftExecutablePythcripts,lollow TheSebestPractices:1)Addashebangline(#!/usr/usr/bin/envpython3)tomakethescriptexecutable.2)setpermissionswithchmodwithchmod xyour_script.3)

Numpy數組與使用數組模塊創建的數組有何不同?Numpy數組與使用數組模塊創建的數組有何不同?Apr 24, 2025 pm 03:53 PM

numpyArraysareAreBetterFornumericalialoperations andmulti-demensionaldata,而learthearrayModuleSutableforbasic,內存效率段

Numpy數組的使用與使用Python中的數組模塊陣列相比如何?Numpy數組的使用與使用Python中的數組模塊陣列相比如何?Apr 24, 2025 pm 03:49 PM

numpyArraySareAreBetterForHeAvyNumericalComputing,而lelethearRayModulesiutable-usemoblemory-connerage-inderabledsswithSimpleDatateTypes.1)NumpyArsofferVerverVerverVerverVersAtility andPerformanceForlargedForlargedAtatasetSetsAtsAndAtasEndCompleXoper.2)

CTYPES模塊與Python中的數組有何關係?CTYPES模塊與Python中的數組有何關係?Apr 24, 2025 pm 03:45 PM

ctypesallowscreatingingangandmanipulatingc-stylarraysinpython.1)usectypestoInterfacewithClibrariesForperfermance.2)createc-stylec-stylec-stylarraysfornumericalcomputations.3)passarraystocfunctions foreforfunctionsforeffortions.however.however,However,HoweverofiousofmemoryManageManiverage,Pressiveo,Pressivero

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

SAP NetWeaver Server Adapter for Eclipse

SAP NetWeaver Server Adapter for Eclipse

將Eclipse與SAP NetWeaver應用伺服器整合。

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

VSCode Windows 64位元 下載

VSCode Windows 64位元 下載

微軟推出的免費、功能強大的一款IDE編輯器