摘要:中關于線程的標準庫是,之前在版本中的在之后更名為,無論是還是都應該盡量避免使用較為底層的而應該使用。而與線程相比,協程尤其是結合事件循環無論在編程模型還是語法上,看起來都是非常友好的單線程同步過程。
項目地址:https://git.io/pytips
要說到線程(Thread)與協程(Coroutine)似乎總是需要從并行(Parallelism)與并發(Concurrency)談起,關于并行與并發的問題,Rob Pike 用 Golang 小地鼠燒書的例子給出了非常生動形象的說明。簡單來說并行就是我們現實世界運行的樣子,每個人都是獨立的執行單元,各自完成自己的任務,這對應著計算機中的分布式(多臺計算機)或多核(多個CPU)運作模式;而對于并發,我看到最生動的解釋來自Quora 上 Jan Christian Meyer 回答的這張圖:
并發對應計算機中充分利用單核(一個CPU)實現(看起來)多個任務同時執行。我們在這里將要討論的 Python 中的線程與協程僅是基于單核的并發實現,隨便去網上搜一搜(Thread vs Coroutine)可以找到一大批關于它們性能的爭論、benchmark,這次話題的目的不在于討論誰好誰壞,套用一句非常套路的話來說,拋開應用場景爭好壞都是耍流氓。當然在硬件支持的條件下(多核)也可以利用線程和協程實現并行計算,而且 Python 2.6 之后新增了標準庫 multiprocessing (PEP 371)突破了 GIL 的限制可以充分利用多核,但由于協程是基于單個線程的,因此多進程的并行對它們來說情況是類似的,因此這里只討論單核并發的實現。
要了解線程以及協程的原理和由來可以查看參考鏈接中的前兩篇文章。Python 3.5 中關于線程的標準庫是 threading,之前在 2.x 版本中的 thread 在 3.x 之后更名為 _thread ,無論是2.7還是3.5都應該盡量避免使用較為底層的 thread/_thread 而應該使用 threading。
創建一個線程可以通過實例化一個 threading.Thread 對象:
from threading import Thread import time def _sum(x, y): print("Compute {} + {}...".format(x, y)) time.sleep(2.0) return x+y def compute_sum(x, y): result = _sum(x, y) print("{} + {} = {}".format(x, y, result)) start = time.time() threads = [ Thread(target=compute_sum, args=(0,0)), Thread(target=compute_sum, args=(1,1)), Thread(target=compute_sum, args=(2,2)), ] for t in threads: t.start() for t in threads: t.join() print("Total elapsed time {} s".format(time.time() - start)) # Do not use Thread start = time.time() compute_sum(0,0) compute_sum(1,1) compute_sum(2,2) print("Total elapsed time {} s".format(time.time() - start))
Compute 0 + 0... Compute 1 + 1... Compute 2 + 2... 0 + 0 = 0 1 + 1 = 2 2 + 2 = 4 Total elapsed time 2.002729892730713 s Compute 0 + 0... 0 + 0 = 0 Compute 1 + 1... 1 + 1 = 2 Compute 2 + 2... 2 + 2 = 4 Total elapsed time 6.004806041717529 s
除了通過將函數傳遞給 Thread 創建線程實例之外,還可以直接繼承 Thread 類:
from threading import Thread import time class ComputeSum(Thread): def __init__(self, x, y): super().__init__() self.x = x self.y = y def run(self): result = self._sum(self.x, self.y) print("{} + {} = {}".format(self.x, self.y, result)) def _sum(self, x, y): print("Compute {} + {}...".format(x, y)) time.sleep(2.0) return x+y threads = [ComputeSum(0,0), ComputeSum(1,1), ComputeSum(2,2)] start = time.time() for t in threads: t.start() for t in threads: t.join() print("Total elapsed time {} s".format(time.time() - start))
Compute 0 + 0... Compute 1 + 1... Compute 2 + 2... 0 + 0 = 0 1 + 1 = 2 2 + 2 = 4 Total elapsed time 2.001662015914917 s
根據上面代碼執行的結果可以發現,compute_sum/t.run 函數的執行是按照 start() 的順序,但 _sum 結果的輸出順序卻是隨機的。因為 _sum 中加入了 time.sleep(2.0) ,讓程序執行到這里就會進入阻塞狀態,但是幾個線程的執行看起來卻像是同時進行的(并發)。
有時候我們既需要并發地“跳過“阻塞的部分,又需要有序地執行其它部分,例如操作共享數據的時候,這時就需要用到”鎖“。在上述”求和線程“的例子中,假設每次求和都需要加上額外的 _base 并把計算結果累積到 _base 中。盡管這個例子不太恰當,但它說明了線程鎖的用途:
from threading import Thread, Lock import time _base = 1 _lock = Lock() class ComputeSum(Thread): def __init__(self, x, y): super().__init__() self.x = x self.y = y def run(self): result = self._sum(self.x, self.y) print("{} + {} + base = {}".format(self.x, self.y, result)) def _sum(self, x, y): print("Compute {} + {}...".format(x, y)) time.sleep(2.0) global _base with _lock: result = x + y + _base _base = result return result threads = [ComputeSum(0,0), ComputeSum(1,1), ComputeSum(2,2)] start = time.time() for t in threads: t.start() for t in threads: t.join() print("Total elapsed time {} s".format(time.time() - start))
Compute 0 + 0... Compute 1 + 1... Compute 2 + 2... 0 + 0 + base = 1 1 + 1 + base = 3 2 + 2 + base = 7 Total elapsed time 2.0064051151275635 s
這里用上下文管理器來管理鎖的獲取和釋放,相當于:
_lock.acquire() try: result = x + y + _base _base = result finally: _lock.release()
死鎖
線程的一大問題就是通過加鎖來”搶奪“共享資源的時候有可能造成死鎖,例如下面的程序:
from threading import Lock _base_lock = Lock() _pos_lock = Lock() _base = 1 def _sum(x, y): # Time 1 with _base_lock: # Time 3 with _pos_lock: result = x + y return result def _minus(x, y): # Time 0 with _pos_lock: # Time 2 with _base_lock: result = x - y return result
由于線程的調度執行順序是不確定的,在執行上面兩個線程 _sum/_minus 的時候就有可能出現注釋中所標注的時間順序,即 # Time 0 的時候運行到 with _pos_lock 獲取了 _pos_lock 鎖,而接下來由于阻塞馬上切換到了 _sum 中的 # Time 1 ,并獲取了 _base_lock,接下來由于兩個線程互相鎖定了彼此需要的下一個鎖,將會導致死鎖,即程序無法繼續運行。根據 我是一個線程 中所描述的,為了避免死鎖,需要所有的線程按照指定的算法(或優先級)來進行加鎖操作。不管怎么說,死鎖問題都是一件非常傷腦筋的事,原因之一在于不管線程實現的是并發還是并行,在編程模型和語法上看起來都是并行的,而我們的大腦雖然是一個(內隱的)絕對并行加工的機器,卻非常不善于將并行過程具象化(至少在未經足夠訓練的時候)。而與線程相比,協程(尤其是結合事件循環)無論在編程模型還是語法上,看起來都是非常友好的單線程同步過程。后面第二部分我們再來討論 Python 中協程是如何從”小三“一步步扶正上位的:D。
歡迎關注公眾號 PyHub 每日推送
參考Python 中的進程、線程、協程、同步、異步、回調
我是一個線程
Concurrency is not Parallelism
A Curious Course on Coroutines and Concurrency
PyDocs: 17.1. threading — Thread-based parallelism
PyDocs: 18.5.3. Tasks and coroutines
[譯] Python 3.5 協程究竟是個啥
協程的好處是什么? - crazybie 的回答
Py3-cookbook:第十二章:并發編程
Quora: What are the differences between parallel, concurrent and asynchronous programming?
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/37867.html
摘要:項目地址我之前翻譯了協程原理這篇文章之后嘗試用了模式下的協程進行異步開發,確實感受到協程所帶來的好處至少是語法上的。 項目地址:https://git.io/pytips 我之前翻譯了Python 3.5 協程原理這篇文章之后嘗試用了 Tornado + Motor 模式下的協程進行異步開發,確實感受到協程所帶來的好處(至少是語法上的:D)。至于協程的 async/await 語法是如...
摘要:所以與多線程相比,線程的數量越多,協程性能的優勢越明顯。值得一提的是,在此過程中,只有一個線程在執行,因此這與多線程的概念是不一樣的。 真正有知識的人的成長過程,就像麥穗的成長過程:麥穗空的時候,麥子長得很快,麥穗驕傲地高高昂起,但是,麥穗成熟飽滿時,它們開始謙虛,垂下麥芒。 ——蒙田《蒙田隨筆全集》 上篇論述了關于python多線程是否是雞肋的問題,得到了一些網友的認可,當然也有...
摘要:項目地址提供兩種內置排序方法,一個是只針對的原地排序方法,另一個是針對所有可迭代對象的非原地排序方法。 項目地址:https://git.io/pytips Python 提供兩種內置排序方法,一個是只針對 List 的原地(in-place)排序方法 list.sort(),另一個是針對所有可迭代對象的非原地排序方法 sorted()。 所謂原地排序是指會立即改變被排序的列表對象,就...
摘要:事件循環是異步編程的底層基石。對事件集合進行輪詢,調用回調函數等一輪事件循環結束,循環往復。協程直接利用代碼的執行位置來表示狀態,而回調則是維護了一堆數據結構來處理狀態。時代的協程技術主要是,另一個比較小眾。 Coding Crush Python開發工程師 主要負責豈安科技業務風險情報系統redq。 引言 1.1. 存儲器山 存儲器山是 Randal Bryant 在《深入...
摘要:上一篇文章第二章實戰演練開發網站第五節輸出相應函數下一篇文章第二章實戰演練開發網站第七節安全機制有兩種方式可改變同步的處理流程異步化針對的處理函數使用修飾器,將默認的同步機制改為異步機制。使用異步對象處理耗時操作,比如本例的。 上一篇文章:Python:Tornado 第二章:實戰演練:開發Tornado網站:第五節:RequestHandler:輸出相應函數下一篇文章:Python:...
閱讀 2106·2021-11-23 09:51
閱讀 2844·2021-11-22 15:35
閱讀 2942·2019-08-30 15:53
閱讀 1044·2019-08-30 14:04
閱讀 3283·2019-08-29 12:39
閱讀 1811·2019-08-28 17:57
閱讀 1099·2019-08-26 13:39
閱讀 559·2019-08-26 13:34