在Python開發領域,GIL(Global Interpreter Lock)一直是一個廣受關注的技術話題。在3.13已經默認將GIL去除,在詳細介紹3.13的更親前,我們先要留了解GIL的技術本質、其對Python程序性能的影響。本文將主要基于CPython(用C語言實現的Python解釋器,也是目前應用最廣泛的Python解釋器)展開討論。
GIL的技術定義
GIL(Global Interpreter Lock)是CPython解釋器中的一個互斥鎖(mutex)機制,其核心作用是保護Python對象的訪問,防止多個本地線程同時執行Python字節碼。從技術實現角度來看,GIL確保在任一時刻只有一個線程能在Python解釋器中執行代碼。
在實際運行過程中,假設程序創建了10個并發線程,在任一時刻檢查CPU核心時,只能觀察到一個線程在執行。每個線程在執行特定數量的字節碼操作后,都會釋放GIL并退出當前核心。在CPython的默認實現中,每個線程可以在釋放GIL之前執行100個字節碼指令。GIL釋放后,其他等待線程中的一個將獲得鎖并開始執行。
從實現機制來看,GIL可以被視為一個線程執行令牌,線程必須獲取這個令牌才能執行字節碼指令。
GIL的技術必要性
GIL的存在與CPython的內存管理機制密切相關。要理解GIL的必要性,需要先了解CPython的內存管理實現原理。
CPython采用引用計數(reference counting)作為其主要的內存管理機制。系統會為每個Python對象維護一個引用計數器,記錄指向該對象的引用數量。當引用計數降至零時,對象占用的內存將被立即釋放。
在多線程環境下對同一Python對象的訪問在多線程場景下,考慮如下情況:假設有3個線程同時持有對同一Python對象的引用,此時該對象的引用計數為3。當一個線程釋放對該對象的引用時,計數值降為2。
這里存在一個關鍵的技術問題:如果兩個線程同時釋放對該對象的引用,會出現競爭條件(race condition)。在這種情況下,引用計數可能只會減少一次而不是預期的兩次,導致最終引用計數為2而不是1。這將導致對象永遠保持非零引用計數,使得垃圾回收器無法回收該對象,最終造成內存泄漏。
GIL的設計正是為了解決這個問題。通過確保同一時刻只有一個線程在執行,GIL有效防止了多線程環境下的引用計數競爭問題。這種機制保證了對Python對象的訪問是串行的,從而維護了解釋器內部狀態的一致性。
GIL的技術局限性
GIL雖然解決了內存管理的并發問題,但同時也帶來了性能方面的技術挑戰。
最主要的性能開銷來自于線程執行時頻繁的GIL獲取和釋放操作。這種額外的同步開銷導致了多線程程序在某些場景下的性能反而低于單線程程序。
以下是具體的性能測試示例。首先是單線程實現:
importtime defmyfunc(): """ 執行5億次迭代的高精度計時測試""" before_time=time.perf_counter() for_inrange(500000000): pass after_time=time.perf_counter() elapsed_time=after_time-before_time print(f"Time taken in total: {elapsed_time:.6f} seconds") if__name__=="__main__": myfunc()
單線程執行結果顯示耗時約8.426秒
對比使用兩個線程的實現:
importtime importthreading defworker(iterations, thread_id): """ 執行指定迭代次數的工作線程函數參數: iterations (int): 迭代執行次數thread_id (int): 線程標識號""" print(f"Thread {thread_id} starting.") for_inrange(iterations): pass print(f"Thread {thread_id} finished.") defmyfunc(): """ 將5億次迭代平均分配給兩個線程執行的性能測試""" total_iterations=500000000 half_iterations=total_iterations//2 thread1=threading.Thread(target=worker, args=(half_iterations, 1)) thread2=threading.Thread(target=worker, args=(half_iterations, 2)) print("Starting threads...") before_time=time.perf_counter() thread1.start() thread2.start() thread1.join() thread2.join() after_time=time.perf_counter() elapsed_time=after_time-before_time print(f"Time taken in total: {elapsed_time:.6f} seconds") if__name__=="__main__": myfunc()
多線程執行結果顯示耗時約11.256秒
這個性能測試清晰地展示了GIL對Python多線程執行效率的影響,同時也說明了Python在實現真正的線程級并行計算時所面臨的技術限制。
3.13 前的技術解決方案
針對GIL帶來的限制,目前有多種技術解決方案,但每種方案都有其特定的應用場景和局限性:
多進程方案: 通過Python的
multiprocessing
模塊,可以創建多個獨立的Python解釋器進程,每個進程都擁有獨立的GIL和內存空間,從而實現真正的并行計算。
異步編程: 對于I/O密集型應用,可以使用異步編程模型(如asyncio)實現并發,這種方式可以在單線程環境下高效處理并發任務,降低GIL的影響。
替代性Python實現: 一些Python的其他實現(如Jython、IronPython、PyPy)采用了不同的內存管理機制,不依賴GIL。這些實現通過不同的技術方案避免了GIL的限制,但可能會帶來其他方面的權衡。
總結
GIL是CPython實現中的一個核心設計決策,它在保證內存管理安全性的同時也帶來了并行計算效率的限制。在實際開發中,需要根據具體的應用場景選擇合適的技術方案來規避或降低GIL的影響。理解GIL的技術本質和局限性,對于設計高性能的Python應用系統具有重要意義。
PEP 703 提出的移除 GIL 的設計,不僅解決了 GIL 帶來的多線程性能瓶頸,還通過細粒度鎖、樂觀鎖、RCU 和 STW 等多種機制,在性能和線程安全之間實現了巧妙的平衡。但是根據 Python 路線圖顯示,至少要到 2028 年,GIL 才會被默認禁用。所以目前來看的話了解GIL還是十分有必要的。
https://avoid.overfit.cn/post/3545a1aabf5a4452861804a1c5340ac0
作者:Sambhu Nampoothiri G