c++ 协程_理解Python协程(Coroutine)
由于GIL的存在,導致Python多線程性能甚至比單線程更糟。
GIL: 全局解釋器鎖(英語:Global Interpreter Lock,縮寫GIL),是計算機程序設計語言解釋器用于同步線程的一種機制,它使得任何時刻僅有一個線程在執行。[1]即便在多核心處理器上,使用 GIL 的解釋器也只允許同一時間執行一個線程。于是出現了協程(Coroutine)這么個東西。
協程: 協程,又稱微線程,纖程,英文名Coroutine。協程的作用,是在執行函數A時,可以隨時中斷,去執行函數B,然后中斷繼續執行函數A(可以自由切換)。但這一過程并不是函數調用(沒有調用語句),這一整個過程看似像多線程,然而協程只有一個線程執行.協程由于由程序主動控制切換,沒有線程切換的開銷,所以執行效率極高。對于IO密集型任務非常適用,如果是cpu密集型,推薦多進程+協程的方式。
在Python3.4之前,官方沒有對協程的支持,存在一些三方庫的實現,比如gevent和Tornado。3.4之后就內置了asyncio標準庫,官方真正實現了協程這一特性。
而Python對協程的支持,是通過Generator實現的,協程是遵循某些規則的生成器。因此,我們在了解協程之前,我們先要學習生成器。
生成器(Generator)
我們這里主要討論yield和yield from這兩個表達式,這兩個表達式和協程的實現息息相關。
- Python2.5中引入yield表達式,參見PEP342
- Python3.3中增加yield from語法,參見PEP380,
方法中包含yield表達式后,Python會將其視作generator對象,不再是普通的方法。
yield表達式的使用
我們先來看該表達式的具體使用:
def test():print("generator start")n = 1while True:yield_expression_value = yield nprint("yield_expression_value = %d" % yield_expression_value)n += 1# ①創建generator對象 generator = test() print(type(generator))print("n---------------n")# ②啟動generator next_result = generator.__next__() print("next_result = %d" % next_result)print("n---------------n")# ③發送值給yield表達式 send_result = generator.send(666) print("send_result = %d" % send_result)執行結果:
<class 'generator'>---------------generator start next_result = 1---------------yield_expression_value = 666 send_result = 2方法說明:
- __next__()方法: 作用是啟動或者恢復generator的執行,相當于send(None)
- send(value)方法:作用是發送值給yield表達式。啟動generator則是調用send(None)
執行結果的說明:
- ①創建generator對象:包含yield表達式的函數將不再是一個函數,調用之后將會返回generator對象
- ②啟動generator:使用生成器之前需要先調用__next__或者send(None),否則將報錯。啟動generator后,代碼將執行到yield出現的位置,也就是執行到yield n,然后將n傳遞到generator.__next__()這行的返回值。(注意,生成器執行到yield n后將暫停在這里,直到下一次生成器被啟動)
- ③發送值給yield表達式:調用send方法可以發送值給yield表達式,同時恢復生成器的執行。生成器從上次中斷的位置繼續向下執行,然后遇到下一個yield,生成器再次暫停,切換到主函數打印出send_result。
理解這個demo的關鍵是:生成器啟動或恢復執行一次,將會在yield處暫停。上面的第②步僅僅執行到了yield n,并沒有執行到賦值語句,到了第③步,生成器恢復執行才給yield_expression_value賦值。
生產者和消費者模型
上面的例子中,代碼中斷–>切換執行,體現出了協程的部分特點。
我們再舉一個生產者、消費者的例子,這個例子來自廖雪峰的Python教程:
傳統的生產者-消費者模型是一個線程寫消息,一個線程取消息,通過鎖機制控制隊列和等待,但一不小心就可能死鎖。現在改用協程,生產者生產消息后,直接通過yield跳轉到消費者開始執行,待消費者執行完畢后,切換回生產者繼續生產,效率極高。def consumer():print("[CONSUMER] start")r = 'start'while True:n = yield rif not n:print("n is empty")continueprint("[CONSUMER] Consumer is consuming %s" % n)r = "200 ok"def producer(c):# 啟動generatorstart_value = c.send(None)print(start_value)n = 0while n < 3:n += 1print("[PRODUCER] Producer is producing %d" % n)r = c.send(n)print('[PRODUCER] Consumer return: %s' % r)# 關閉generatorc.close()# 創建生成器 c = consumer() # 傳入generator producer(c)
執行結果:
[CONSUMER] start start [PRODUCER] producer is producing 1 [CONSUMER] consumer is consuming 1 [PRODUCER] Consumer return: 200 ok [PRODUCER] producer is producing 2 [CONSUMER] consumer is consuming 2 [PRODUCER] Consumer return: 200 ok [PRODUCER] producer is producing 3 [CONSUMER] consumer is consuming 3 [PRODUCER] Consumer return: 200 ok注意到consumer函數是一個generator,把一個consumer傳入produce后:yield from表達式
Python3.3版本新增yield from語法,新語法用于將一個生成器部分操作委托給另一個生成器。此外,允許子生成器(即yield from后的“參數”)返回一個值,該值可供委派生成器(即包含yield from的生成器)使用。并且在委派生成器中,可對子生成器進行優化。
我們先來看最簡單的應用,例如:
# 子生成器 def test(n):i = 0while i < n:yield ii += 1# 委派生成器 def test_yield_from(n):print("test_yield_from start")yield from test(n)print("test_yield_from end")for i in test_yield_from(3):print(i)輸出:
test_yield_from start 0 1 2 test_yield_from end這里我們僅僅給這個生成器添加了一些打印,如果是正式的代碼中,你可以添加正常的執行邏輯。
如果上面的test_yield_from函數中有兩個yield from語句,將串行執行。比如將上面的test_yield_from函數改寫成這樣:
def test_yield_from(n):print("test_yield_from start")yield from test(n)print("test_yield_from doing")yield from test(n)print("test_yield_from end")將輸出:
test_yield_from start 0 1 2 test_yield_from doing 0 1 2 test_yield_from end在這里,yield from起到的作用相當于下面寫法的簡寫形式
for item in test(n):yield item看起來這個yield from也沒做什么大不了的事,其實它還幫我們處理了異常之類的。具體可以看stackoverflow上的這個問題:In practice, what are the main uses for the new “yield from” syntax in Python 3.3?
協程(Coroutine)
- Python3.4開始,新增了asyncio相關的API,語法使用`@asyncio.coroutine和yield from`實現協程
- Python3.5中引入async/await語法,參見PEP492
我們先來看Python3.4的實現。
`@asyncio.coroutine`
Python3.4中,使用`@asyncio.coroutine`裝飾的函數稱為協程。不過沒有從語法層面進行嚴格約束。
對裝飾器不了解的小伙伴可以看我的上一篇博客–《理解Python裝飾器》對于Python原生支持的協程來說,Python對協程和生成器做了一些區分,便于消除這兩個不同但相關的概念的歧義:
- 標記了`@asyncio.coroutine裝飾器的函數稱為協程函數,iscoroutinefunction()`方法返回True
- 調用協程函數返回的對象稱為協程對象,iscoroutine()函數返回True
舉個栗子,我們給上面yield from的demo中添加`@asyncio.coroutine`:
import asyncio...@asyncio.coroutine def test_yield_from(n):...# 是否是協程函數 print(asyncio.iscoroutinefunction(test_yield_from)) # 是否是協程對象 print(asyncio.iscoroutine(test_yield_from(3)))毫無疑問輸出結果是True。
可以看下`@asyncio.coroutine`的源碼中查看其做了什么,我將其源碼簡化下,大致如下:
import functools import types import inspectdef coroutine(func):# 判斷是否是生成器if inspect.isgeneratorfunction(func):coro = funcelse:# 將普通函數變成generator@functools.wraps(func)def coro(*args, **kw):res = func(*args, **kw)res = yield from resreturn res# 將generator轉換成coroutinewrapper = types.coroutine(coro)# For iscoroutinefunction().wrapper._is_coroutine = Truereturn wrapper將這個裝飾器標記在一個生成器上,就會將其轉換成coroutine。
然后,我們來實際使用下`@asyncio.coroutine和yield from`:
import asyncio@asyncio.coroutine def compute(x, y):print("Compute %s + %s ..." % (x, y))yield from asyncio.sleep(1.0)return x + y@asyncio.coroutine def print_sum(x, y):result = yield from compute(x, y)print("%s + %s = %s" % (x, y, result))loop = asyncio.get_event_loop() print("start") # 中斷調用,直到協程執行結束 loop.run_until_complete(print_sum(1, 2)) print("end") loop.close()執行結果:
start Compute 1 + 2 ... 1 + 2 = 3 endprint_sum這個協程中調用了子協程compute,它將等待compute執行結束才返回結果。
這個demo點調用流程如下圖:
EventLoop將會把print_sum封裝成Task對象
流程圖展示了這個demo的控制流程,不過沒有展示其全部細節。比如其中“暫?!钡?s,實際上創建了一個future對象, 然后通過BaseEventLoop.call_later()在1s后喚醒這個任務。
值得注意的是,`@asyncio.coroutine`將在Python3.10版本中移除。
async/await
Python3.5開始引入async/await語法(PEP 492),用來簡化協程的使用并且便于理解。
async/await實際上只是`@asyncio.coroutine和yield from`的語法糖:
- 把`@asyncio.coroutine替換為async`
- 把yield from替換為await
即可。
比如上面的例子:
import asyncioasync def compute(x, y):print("Compute %s + %s ..." % (x, y))await asyncio.sleep(1.0)return x + yasync def print_sum(x, y):result = await compute(x, y)print("%s + %s = %s" % (x, y, result))loop = asyncio.get_event_loop() print("start") loop.run_until_complete(print_sum(1, 2)) print("end") loop.close()我們再來看一個asyncio中Future的例子:
import asynciofuture = asyncio.Future()async def coro1():print("wait 1 second")await asyncio.sleep(1)print("set_result")future.set_result('data')async def coro2():result = await futureprint(result)loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait([coro1()coro2() ])) loop.close()輸出結果:
wait 1 second (大約等待1秒) set_result data這里await后面跟隨的future對象,協程中yield from或者await后面可以調用future對象,其作用是:暫停協程,直到future執行結束或者返回result或拋出異常。
而在我們的例子中,await future必須要等待future.set_result('data')后才能夠結束。將coro2()作為第二個協程可能體現得不夠明顯,可以將協程的調用改成這樣:
loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait([# coro1(),coro2(),coro1() ])) loop.close()輸出的結果仍舊與上面相同。
其實,async這個關鍵字的用法不止能用在函數上,還有async with異步上下文管理器,async for異步迭代器. 對這些感興趣且覺得有用的可以網上找找資料,這里限于篇幅就不過多展開了。
總結
本文就生成器和協程做了一些學習、探究和總結,不過并沒有做過多深入深入的研究。權且作為入門到一個筆記,之后將會嘗試自己實現一下異步API,希望有助于理解學習。
參考鏈接
Python協程 https://thief.one/2017/02/20/Python%E5%8D%8F%E7%A8%8B/http://www.dabeaz.com/coroutines/Coroutines.pdfCoroutines
How the heck does async/await work in Python 3.5
Python3.4協程文檔
Python3.5協程文檔
廖雪峰的Python教程–協程
總結
以上是生活随笔為你收集整理的c++ 协程_理解Python协程(Coroutine)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python的for语句是否只有一种写法
- 下一篇: public 函数_Chapter18: