Python GIL(Global Interpreter Lock)

Python GIL(Global Interpreter Lock)

本文將介紹GIL是什麼,為什麼存在,對Python的影響是什麼,以及一些歷史演進。
本文主要參考David Beazley的2篇分享Inside the Python GILInside the New GIL

Introduce

官方說明

In CPython, the global interpreter lock, or GIL, is a mutex that protects access to Python objects, preventing multiple 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是一個控制Thread存取Python object的mutex
  • GIL能確保同一時間只有一個Thread在Python interpreter內執行
  • GIL存在的原因之一是因為CPython的memory management不是thread-safe

上述多次提到Thread,在Python中的Thread是一個Operating system的Thread,例如POSIX thread, Windows thread。
換言之,Python thread的scheduling與switching皆是由OS處理

Types of Threads

GIL與Thread密不可分,而GIL對不同種類的Thread的關係和影響也不同。
以下分別對兩種Thread分別闡述:

I/O bound threads

由圖得知(圖片來源:Inside the Python GIL):

  • 在I/O bound threads執行前,並須先Acquire GIL
  • 遇到I/O block時,會Release GIL
  • 當GIL不被任何Thread持有時,則其他Thread有機會Acquire GIL
  • 換言之,在I/O bound threads時,Multiple thread會比Single thread速度快

CPU bound threads

由圖得知(圖片來源:Inside the Python GIL):

  • CPU bound不像I/O bound threads會被I/O block
  • Python interpreter會定期執行Check
    • 預設是100 interpreter ticks執行一次Check
      • Tick:
        • 由很多個Python interpreter instructions組成
        • 不是基於時間來計算的,所以每個Tick的執行時間不相同
    • 使用sys.setcheckinterval()更改此值
    • 執行Check時讓當前持有GIL的Thread release GIL,則其他Thread有機會拿到GIL
  • 如果Main thread拿到了GIL,則會額外處理Signal,例如Interrupt signal
    • 結合Python thread沒有Thread的scheduling能力,引發另一個問題
      • 只有在Main thread拿到GIL時,該Program的Signal才能被處理
      • 如果Main thread被不可中斷的Thread-join或Lock block時,Main thread永遠不會被OS安排到可被執行的狀態
      • 所以Signal也永遠不會被處理

GIL Battle

如圖所示(圖片來源:Inside the Python GIL)

  • T1/T2皆為CPU bound threads,且執行在不同的CPU core上
  • 當T1在第一次Acquire GIL後,在每次的Check過程中,T1 release GIL之後,馬上又Acquire GIL並繼續執行,導致T2一直沒有機會Acquire GIL
  • GIL Battle是因為下述兩個原則衝突導致
    • OS的Thread scheduling的目的是要發揮Multiple cores的優勢
    • GIL讓Python interpreter同時只能執行一個Thread
  • 對I/O bound threads的影響
    • 當I/O bound thread接收到I/O block結束的信號時,但另一個Core的CPU bound thread不斷的release/Acquire GIL
    • I/O bound thread也就無法讀取I/O的結果
    • 引發Priority inversion
      • 在OS中,I/O bound thread的執行優先權高於CPU bound thread
      • 但在發生上述情況時,I/O bound thread反而不會優先於CPU bound thread被執行完,此情況稱為Priority inversion

New GIL in Python3.2

Python 3.2的GIL進行了改善:

  • Interpreter執行區間從Tick-based變更為Time-based
    • 移除Tick機制,以及sys.setcheckinterval()
  • 顯著的降低了Thrashing和Signaling的消耗

New Thread Switching

  • 使用全局變量gil_drop_request決定是否進行Release GIL
  • Running thread release GIL
    • 被強制的Release GIL: gil_drop_request由0設定為1
    • 自主的Release GIL: 進行I/O block或Sleep
  • Suspended thread的狀態轉換
    • 開始執行後,發現已經有其他Thread正在執行,無法Acquire GIL,則進入Suspended狀態,並設定該狀態維持T時長
      • Case 1: 在T時間內接收到Release GIL signal後Acquire GIL
      • Case 2: T時間內仍未接收到Release GIL signal,提出將gil_drop_request由0設定為1,則進入Suspended狀態,並設定該狀態維持T時長
        • Case 2.1: 在T時間內接收到Release GIL signal後Acquire GIL
        • Case 2.2: OS將Release GIL signal發送給其他Suspended thread,則自己回到Case 2等完T時長
    • Thread在Acquire GIL的一段時間內,不會馬上被強制Release GIL
  • 若有多個Thread提出將gil_drop_request由0設定為1時,只有第一個要求會完成設置
  • 提出要求的Thread,不一定可以Acquire GIL,可能被其他也在等待的Thread Acquire GIL
  • T時長:
    • 預設為5 milliseconds (0.005s)
    • 使用sys.setswitchinterval()設定該值
  • Time-based機制消除了GIL Battle
  • Case 2參考(圖片來源:Inside the New GIL)

Interesting Features

  • 新的GIL允許Acquire GIL的Thread無視其他Thread或I/O priority,持有GIL一個T時長
  • 所以一個CPU-bound thread在整個操作週期中,會多次主動釋放GIL(例如多次Receive/Send data),而在每次Acquire GIL前最少都得等待一個T時長,導致I/O bound thread的總反應時間增加

Reference