摘要:其次,解釋器的主循環,一個名為的函數,讀取字節碼并逐個執行其中的指令。所有線程都運行相同的代碼,并以相同的方式定期從它們獲取鎖定。無論如何,其他線程無法并行運行。
概述
如今我也是使用Python寫代碼好多年了,但是我卻很少關心GIL的內部機制,導致在寫Python多線程程序的時候。今天我們就來看看CPython的源代碼,探索一下GIL的源碼,了解為什么Python里要存在這個GIL,過程中我會給出一些示例來幫助大家更好的理解GIL。
GIL概覽有如下代碼:
static PyThread_type_lock interpreter_lock = 0; /* This is the GIL */
這行代碼位于Python2.7源碼ceval.c文件里。在類Unix操作系統中,PyThread_type_lock對應C語言里的mutex_t類型。在Python解釋器開始運行時初始化這個變量
void PyEval_InitThreads(void) { interpreter_lock = PyThread_allocate_lock(); PyThread_acquire_lock(interpreter_lock); }
所有Python解釋器里執行的c代碼都必須獲取這個鎖,作者一開始為求簡單,所以使用這種單線程的方式,后來每次想移除時,都發現代價太高了。
GIL對程序中的線程的影響很簡單,你可以在手背上寫下這個原則:“一個線程運行Python,而另外一個線程正在等待I / O.”Python代碼可以使用threading.Lock或者其他同步對象,來釋放CPU占用,讓其他程序得以執行。
什么時候線程切換? 每當線程開始休眠或等待網絡I / O時,另一個線程都有機會獲取GIL并執行Python代碼。CPython還具有搶先式多任務處理:如果一個線程在Python 2中不間斷地運行1000個字節碼指令,或者在Python 3中運行15毫秒,那么它就會放棄GIL而另一個線程可能會運行。
協作式多任務每當運行一個任務,比如網絡I/O,持續的時間很長或者無法確定運行時間,這時可以放棄GIL,這樣另一個線程就可以接受并運行Python。 這種行為稱為協同多任務,它允許并發; 許多線程可以同時等待不同的事件。
假設有兩個鏈接socket的線程
def do_connect(): s = socket.socket() s.connect(("python.org", 80)) # drop the GIL for i in range(2): t = threading.Thread(target=do_connect) t.start()
這兩個線程中一次只有一個可以執行Python,但是一旦線程開始連接,它就會丟棄GIL,以便其他線程可以運行。這意味著兩個線程都可以等待它們的套接字同時連接,他們可以在相同的時間內完成更多的工作。
接下來,讓我們打開Python的源碼,來看看內部是如何實現的(位于socketmodule.c文件里):
static PyObject * sock_connect(PySocketSockObject *s, PyObject *addro) { sock_addr_t addrbuf; int addrlen; int res; /* convert (host, port) tuple to C address */ getsockaddrarg(s, addro, SAS2SA(&addrbuf), &addrlen); Py_BEGIN_ALLOW_THREADS res = connect(s->sock_fd, addr, addrlen); Py_END_ALLOW_THREADS /* error handling and so on .... */ }
Py_BEGIN_ALLOW_THREADS宏指令用于釋放GIL,他的定義很簡單:
PyThread_release_lock(interpreter_lock);
Py_END_ALLOW_THREADS用于獲取GIL鎖,這時,當前現在有可能會卡住,等待其他現在釋放GIL鎖。
優先權式多任務Python線程可以自愿釋放GIL,但它也可以搶先獲取GIL。
讓我們回顧一下如何執行Python。 您的程序分兩個階段運行。 首先,您的Python文本被編譯為更簡單的二進制格式,稱為字節碼。 其次,Python解釋器的主循環,一個名為PyEval_EvalFrameEx()的函數,讀取字節碼并逐個執行其中的指令。當解釋器逐步執行您的字節碼時,它會定期刪除GIL,而不會詢問正在執行其代碼的線程的權限,因此其他線程可以運行:
for (;;) { if (--ticker < 0) { ticker = check_interval; /* Give another thread a chance */ PyThread_release_lock(interpreter_lock); /* Other threads may run now */ PyThread_acquire_lock(interpreter_lock, 1); } bytecode = *next_instr++; switch (bytecode) { /* execute the next instruction ... */ } }
默認情況下,檢查間隔為1000個字節碼。 所有線程都運行相同的代碼,并以相同的方式定期從它們獲取鎖定。 在Python 3中,GIL的實現更復雜,檢查間隔不是固定數量的字節碼,而是15毫秒。 但是,對于您的代碼,這些差異并不重要。
Python線程安全如果某個線程在任何時候都可能丟失GIL,那么您必須使代碼具有線程安全性。 然而,Python程序員對線程安全的看法與C或Java程序員的不同,因為許多Python操作都是原子的。
原子操作的一個示例是在列表上調用sort()。 線程不能在排序過程中被中斷,其他線程永遠不會看到部分排序的列表,也不會在列表排序之前看到過時的數據。 原子操作簡化了我們的生活,但也有驚喜。 例如,+ =似乎比sort()簡單,但+ =不是原子的。 那我們怎么知道哪些操作是原子的,哪些不是?
例如有代碼如下:
n = 0 def foo(): global n n += 1
我們可以使用python的dis模塊獲取這段代碼對應的字節碼:
>>> import dis >>> dis.dis(foo) LOAD_GLOBAL 0 (n) LOAD_CONST 1 (1) INPLACE_ADD STORE_GLOBAL 0 (n)
可以看出,n += 1這行代碼,編譯出了4個字節碼:
將n的值加載到堆棧上
將常量1加載到堆棧上
將堆棧頂部的兩個值相加
將總和存回n
請記住,一個線程的每1000個字節碼被解釋器中斷以釋放GIL。 如果線程不幸運,這可能發生在它將n的值加載到堆棧上以及何時將其存儲回來之間。這樣就容易導致數據丟失:
threads = [] for i in range(100): t = threading.Thread(target=foo) threads.append(t) for t in threads: t.start() for t in threads: t.join() print(n)
通常這段代碼打印100,因為100個線程中的每一個都增加了1。 但有時你會看到99或98,這就是其中一個線程的更新被另一個線程覆蓋。所以,盡管有GIL,你仍然需要鎖來保護共享的可變狀態:
n = 0 lock = threading.Lock() def foo(): global n with lock: n += 1
同樣的,如果我們使用sort()函數:
lst = [4, 1, 3, 2] def foo(): lst.sort()
翻譯成字節碼如下:
>>> dis.dis(foo) LOAD_GLOBAL 0 (lst) LOAD_ATTR 1 (sort) CALL_FUNCTION 0
可以看出,sort()函數被翻譯成了一條指令,執行過程不會被打斷。
將lst的值加載到堆棧上
將其排序方法加載到堆棧上
調用排序方法
即使lst.sort()需要幾個步驟,sort調用本身也是一個字節碼,因此不會被打斷。 我們可以得出結論,我們不需要鎖定sort()。 或者,請遵循一個簡單的規則:始終鎖定共享可變狀態的讀寫。 畢竟,獲取Python中的threading.Lock花銷很低。
雖然GIL不能免除鎖的需要,但它確實意味著不需要細粒度的鎖定。 在像Java這樣的自由線程語言中,程序員努力在盡可能短的時間內鎖定共享數據,以減少線程爭用并允許最大并行度。 但是,由于線程無法并行運行Python,因此細粒度鎖定沒有任何優勢。 只要沒有線程在休眠時持有鎖,I / O或其他一些GIL丟棄操作,你應該使用最粗糙,最簡單的鎖。 無論如何,其他線程無法并行運行。
在諸如網絡請求等I/O型的場景中,使用Python多線程可以帶來很高的性能提升,因為在I/O場景中,大多數線程都在等待I/O以進行接下來的操作,所以即使單CPU,也能大大提高性能。比如下面這樣的代碼:
import threading import requests urls = [...] def worker(): while True: try: url = urls.pop() except IndexError: break # Done. requests.get(url) for _ in range(10): t = threading.Thread(target=worker) t.start()
如上所述,這些線程在等待通過HTTP獲取URL所涉及的每個套接字操作時丟棄GIL,因此它們比單個線程性能更高。
并行如果你的任務一定要多線程才能更好的完成,那么,對于Python來說,多線程是不合適的,這種情況下,你得使用多進程,因為每個進程都是多帶帶的運行環境,并且可以使用多核,但這會帶來更高的性能開銷。下面的代碼就是使用多進程來運行任務,每個進程里只有一個線程。
import os import sys nums =[1 for _ in range(1000000)] chunk_size = len(nums) // 10 readers = [] while nums: chunk, nums = nums[:chunk_size], nums[chunk_size:] reader, writer = os.pipe() if os.fork(): readers.append(reader) # Parent. else: subtotal = 0 for i in chunk: # Intentionally slow code. subtotal += i print("subtotal %d" % subtotal) os.write(writer, str(subtotal).encode()) sys.exit(0) # Parent. total = 0 for reader in readers: subtotal = int(os.read(reader, 1000).decode()) total += subtotal print("Total: %d" % total)
因為每個進程都擁有多帶帶的GIL,所以這段代碼可以在多核CPU上并行執行。
總結由于Python GIL的存在,導致Python中一個進程下的多個線程無法并行執行,在I/O密集型的場景中,多線程依然能帶來比較好的性能,但是在CPU密集型的場景中,多線程無法帶來性能的提升。但同時也是由于GIL的存在,我們在單進程中,線程安全也比較容易達到。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/43882.html
摘要:通過可視化操作,將安全任務靈活編排成掃描流程。失效轉移失效轉移又稱故障切換,指系統中其中一項設備或服務失效而無法運作時,另一項設備或服務即可自動接手原失效系統所執行的工作,在須彌用于保障任務執行過程中的執行狀態。 概要 1.分布式安全服務編排概念 2.須彌(Sumeru)關鍵實現思路 3.應用場景 前言 在筆者理解,安全防御的本質之一是增加攻擊者的攻擊成本,尤其是時間成本,那么從防御...
摘要:回到目錄評論區抽粉絲送書啦歡迎大家在評論區提出意見和建議抽兩位幸運兒送書,實物圖如下開發從入門到精通內容簡介案例教學。 ? 作者主頁:海擁 ? 作者簡介:?CSDN...
摘要:以下為我的真實案例以我真實案例分享,希望給更多決定重新開始的人以鼓勵我已經上班很久了,目前在中軟做軟件測試工程師,月薪,現在回想起來,仍然慶幸我當初的決定。 ?今天跟大家分享我的故事,或許你也曾像他那樣迷茫過。17年軟件工程專業專科畢業之后做了3年的銷售工作,最后決定還是再次提升專業技能,...
摘要:現在,我們將會剖析的工作原理,而最重要的是它和在性能方面的比對加載時間,執行速度,垃圾回收,內存使用,平臺訪問,調試,多線程以及可移植性。目前,是專門圍繞和的使用場景設計的。目前不支持多線程。 原文請查閱這里,略有改動,本文采用知識共享署名 4.0 國際許可協議共享,BY Troland。 本系列持續更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第六章...
閱讀 2103·2023-04-25 20:52
閱讀 2497·2021-09-22 15:22
閱讀 2127·2021-08-09 13:44
閱讀 1771·2019-08-30 13:55
閱讀 2812·2019-08-23 15:42
閱讀 2286·2019-08-23 14:14
閱讀 2879·2019-08-23 13:58
閱讀 3009·2019-08-23 11:49