GIL(Global Interpreter Lock)
Python 的 multi thread 讓人不明所以,為什麼我的電腦明明有四個 CPU,用 multi thread 性能卻沒有增長呢?我們先看一個例子
1 | from threading import Thread |
CountDown
是個 CPU heavy 的函數,但是奇怪的是,明明用了兩個 Threads,耗時卻差不多,都是 4 秒左右。這是怎麼回事?難道我的電腦只有一顆 CPU 在工作嗎?其實不是。如果換成 C++ 寫,速度提升馬上有感。所以問題不是出在電腦,而是 Python 的 multi thread 沒有真正進行併行運算。
然而,Python 的 Thread 確實是實實在在的 Thread。在 Linux 系統中,它封裝了 Pthread (POSIX Thread);在 Windows 系統中,它封裝了 Windows Thread。由於 Python 的 Thread 只是做封裝,所以完全受作業系統管理,包括協調執行時間、資源管理等等。
Python 用了 multi thread 卻沒有性能提升的原因正是因為 GIL。在整個 Python 的 Process 中,只允許同時跑一個 Thread,其他的會被 Lock。因此,本質上,Python 的 Thread 只是“輪流”執行。
為什麼有 GIL?
我們知道 Python 是基於 C 實現的,之所以有 GIL 就和 CPython 有關。CPython 使用 Reference Count 管理記憶體,這用來記錄有多少pointer指向這塊記憶體。當 Reference Count = 0 時,就會釋放記憶體。我們來看個例子:
1 | import sys |
這裡的 3 是因為 a, b 和 sys.getrefcount(a) 的參數都指向同一塊記憶體,所以總共是 3。
如果兩個 Python 的 Threads 同時引用了 a,同時進行 reference count += 1,就會觸發 race condition,最終 reference count 只會加 1。因此,為了避免這樣的風險,Python 設定了一個 GIL。但是,這並不意味著 Python 是天然不用擔心 race condition 的語言。Python 的一行程式可能代表著多行的 bytecode,例如 Python 的 n += 1
,其 bytecode 表示為:
1 | LOAD_CONST 1 (1) |
若在 INPLACE_ADD
前,GIL 被釋放了,而其他 Thread 跑了 LOAD_CONST 1 (1)
,同樣會造成 race condition。我們來看個例子:
1 | import threading |
所以說,GIL 的目的是為了方便 CPython 的開發者,不用顧慮變數記憶體的分配,而不是為了 Python 應用的開發者。我們還是需要使用 lock 等工具,例如下面這個例子:
1 | import threading |
可以繞過 GIL 嗎?
- Python 的 GIL 是 CPython 上的限制,如果要繞過,可以透過 JPython(Java 實現的 Python)等其他實現。
- 把 CPU heavy 的程式碼放在別的語言(例如 C++)實現,然後提供 Python 調用的 API。
總結
- 我們探究了 Python GIL 的原理,它的目的是為了避免記憶體回收的 race condition。
- 雖然 GIL 使得 CPython 更容易開發,但卻沒有真正的 multi threads。不過,我們可以透過其他語言的實作,把 CPU heavy 的程式碼交給 Python 去呼叫,這樣依然可以充分利用 CPU 性能。