首頁  >  文章  >  後端開發  >  python的GIL是什麼? python中GIL的介紹

python的GIL是什麼? python中GIL的介紹

不言
不言轉載
2018-09-30 11:45:234470瀏覽

這篇文章帶給大家的內容是關於python的GIL是什麼? python中GIL的介紹,有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

GIL是什麼

GIL(Global Interpreter Lock)並不是python的特性,而是Python解釋器Cpython引入的一個概念。而python的解釋器不隻隻有Cpython,若解釋器為Jpython,那麼python就沒有GIL。

我們還是來看一下官方給出的解釋:

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 own the GIL on the that tosguantes have own to dependen gren own tosguy gren own tosguy grave own tosgu on the own tosgu​​e .

#一個防止多執行緒並發執行機器碼的一個Mutex(互斥鎖),原因是:Cpython的記憶體管理是not thread-safe

為什麼會有GIL

#由於物理上得限制,各CPU廠商在核心頻率的比賽已經被多核心所取代。為了更有效的利用多核心處理器的效能,就出現了多執行緒的程式設計方式,而隨之帶來的就是執行緒間資料一致性和狀態同步的困難。即使在CPU內部的Cache也不例外,為了有效解決多份快取之間的資料同步時各廠商花費了不少心思,也不可避免的帶來了一定的效能損失。

Python當然也逃不開,為了利用多核心,Python開始支援多執行緒。而解決多執行緒之間

資料完整性和狀態同步的最簡單方法自然就是加鎖。於是有了GIL這把超級大鎖,而當越來越多的程式碼庫開發者接受了這種設定後,他們開始大量依賴這種特性(即預設python內部物件是thread-safe的,無需在實作時考慮額外的記憶體鎖和同步操作)。

慢慢的這種實現方式被發現是蛋痛且低效的。但當大家試著去拆分、去除GIL的時候,發現大量函式庫程式碼開發者已經重度依賴GIL而非常難以去除了。有多難?做個類比,像MySQL這樣的「小專案」為了把Buffer Pool Mutex這把大鎖拆分成各個小鎖也花了從5.5到5.6再到5.7多個大版為期近5年的時間,並且仍在繼續。 MySQL這個背後有公司支援且有固定開發團隊的產品走的如此艱難,那又何況Python這樣核心開發和程式碼貢獻者高度社群化的團隊呢?

所以簡單的說GIL的存在更多的是歷史原因。如果推到重來,多線程的問題還是要面對,但至少會比目前GIL這種方式會更優雅。

GIL的影響

從上文的介紹和官方的定義來看,GIL無疑就是一把全域排他鎖。毫無疑問全域鎖的存在會對多執行緒的效率有不小影響。甚至幾乎等於Python是個單執行緒的程式。那麼讀者就會說了,全域鎖只要釋放的勤快效率也不會差啊。只要在進行耗時的IO操作的時候,能釋放GIL,這樣還是可以提升運作效率的嘛。或者說再差也不會比單線程的效率差吧。理論上是這樣,但實際上呢? Python比你想的更糟。

下面我們就對比下Python在多執行緒和單執行緒下得效率對比。測試方法很簡單,一個循環1億次的計數器函數。一個透過單執行緒執行兩次,一個多執行緒執行。最後比較執行總時間。測試環境為雙核心的Mac pro。註:為了減少執行緒庫本身效能損耗對測試結果的影響,這裡單執行緒的程式碼同樣使用了執行緒。只是順序的執行兩次,模擬單執行緒。

順序執行的單一執行緒(single_thread.py)

#! /usr/bin/python
from threading import Thread
import time
def my_counter():
i = 0
for _ in range(100000000):
i = i + 1
return True
def main():
thread_array = {}
start_time = time.time()
for tid in range(2):
t = Thread(target=my_counter)
t.start()
t.join()
end_time = time.time()
print("Total time: {}".format(end_time - start_time))
if __name__ == '__main__':
main()

同時執行的兩個並發執行緒(multi_thread.py)

#! /usr/bin/python
from threading import Thread
import time
def my_counter():
i = 0
for _ in range(100000000):
i = i + 1
return True
def main():
thread_array = {}
start_time = time.time()
for tid in range(2):
t = Thread(target=my_counter)
t.start()
thread_array[tid] = t
for i in range(2):
thread_array[i].join()
end_time = time.time()
print("Total time: {}".format(end_time - start_time))
if __name__ == '__main__':
main()

python的GIL是什麼? python中GIL的介紹

#可以看到python在多執行緒的情況下居然比單執行緒整整慢了45%。依照先前的分析,即使是有GIL全域鎖的存在,串行化的多執行緒也應該和單執行緒有相同的效率才對。那怎麼會有這麼糟糕的結果呢?

讓我們透過GIL的實作原理來分析這其中的原因。

目前GIL設計的缺陷

基於pcode數量的排程方式

#依照Python社群的想法,作業系統本身的執行緒調度已經非常成熟穩定了,沒有必要自己搞一套。所以Python的執行緒就是C語言的一個pthread,並且透過作業系統調度演算法進行調度(例如linux是CFS)。為了讓各個執行緒能夠平均利用CPU時間,python會計算目前已執行的微程式碼數量,達到一定閾值後就強制釋放GIL。而這時也會觸發一次作業系統的執行緒調度(當然是否真正進行上下文切換由作業系統自主決定)。

虛擬程式碼

while True:
acquire GIL
for i in 1000:
do something
release GIL
/* Give Operating System a chance to do thread scheduling */

这种模式在只有一个CPU核心的情况下毫无问题。任何一个线程被唤起时都能成功获得到GIL(因为只有释放了GIL才会引发线程调度)。但当CPU有多个核心的时候,问题就来了。从伪代码可以看到,从release GIL到acquire GIL之间几乎是没有间隙的。所以当其他在其他核心上的线程被唤醒时,大部分情况下主线程已经又再一次获取到GIL了。这个时候被唤醒执行的线程只能白白的浪费CPU时间,看着另一个线程拿着GIL欢快的执行着。然后达到切换时间后进入待调度状态,再被唤醒,再等待,以此往复恶性循环。

PS:当然这种实现方式是原始而丑陋的,Python的每个版本中也在逐渐改进GIL和线程调度之间的互动关系。例如先尝试持有GIL在做线程上下文切换,在IO等待时释放GIL等尝试。但是无法改变的是GIL的存在使得操作系统线程调度的这个本来就昂贵的操作变得更奢侈了。

为了直观的理解GIL对于多线程带来的性能影响,这里直接借用的一张测试结果图(见下图)。图中表示的是两个线程在双核CPU上得执行情况。两个线程均为CPU密集型运算线程。绿色部分表示该线程在运行,且在执行有用的计算,红色部分为线程被调度唤醒,但是无法获取GIL导致无法进行有效运算等待的时间。

python的GIL是什麼? python中GIL的介紹

由图可见,GIL的存在导致多线程无法很好的立即多核CPU的并发处理能力。

那么Python的IO密集型线程能否从多线程中受益呢?我们来看下面这张测试结果。颜色代表的含义和上图一致。白色部分表示IO线程处于等待。可见,当IO线程收到数据包引起终端切换后,仍然由于一个CPU密集型线程的存在,导致无法获取GIL锁,从而进行无尽的循环等待。

python的GIL是什麼? python中GIL的介紹

简单的总结下就是:Python的多线程在多核CPU上,只对于IO密集型计算产生正面效果;而当有至少有一个CPU密集型线程存在,那么多线程效率会由于GIL而大幅下降。

如何避免受到GIL的影响

用multiprocessing替代Thread

multiprocessing库的出现很大程度上是为了弥补thread库因为GIL而低效的缺陷。它完整的复制了一套thread所提供的接口方便迁移。唯一的不同就是它使用了多进程而不是多线程。每个进程有自己的独立的GIL,因此也不会出现进程之间的GIL争抢。

当然multiprocessing也不是万能良药。它的引入会增加程序实现时线程间数据通讯和同步的困难。就拿计数器来举例子,如果我们要多个线程累加同一个变量,对于thread来说,申明一个global变量,用thread.Lock的context包裹住三行就搞定了。而multiprocessing由于进程之间无法看到对方的数据,只能通过在主线程申明一个Queue,put再get或者用share memory的方法。这个额外的实现成本使得本来就非常痛苦的多线程程序编码,变得更加痛苦了。

用其他解析器

之前也提到了既然GIL只是CPython的产物,那么其他解析器是不是更好呢?没错,像JPython和IronPython这样的解析器由于实现语言的特性,他们不需要GIL的帮助。然而由于用了Java/C#用于解析器实现,他们也失去了利用社区众多C语言模块有用特性的机会。所以这些解析器也因此一直都比较小众。毕竟功能和性能大家在初期都会选择前者,Done is better than perfect。

总结

Python GIL其实是功能和性能之间权衡后的产物,它尤其存在的合理性,也有较难改变的客观因素。从本分的分析中,我们可以做以下一些简单的总结:

  • 因为GIL的存在,只有IO Bound场景下得多线程会得到较好的性能

  • 如果对并行计算性能较高的程序可以考虑把核心部分也成C模块,或者索性用其他语言实现

  • GIL在较长一段时间内将会继续存在,但是会不断对其进行改进

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

陳述:
本文轉載於:cnblogs.com。如有侵權,請聯絡admin@php.cn刪除