二十五、深入Python中的協程
@Author: Runsen
一說並發,你肯定想到了多線程+進程模型,確實,多線程+進程,正是解決並發問題的經典模型之一。但對於多核CPU,利用多進程+協程的方式,能充分利用CPU,獲得極高的性能。協程也是實現並發編程的一種方式。
協程
協程:是單線程下的並發,又稱微線程。英文名是Coroutine。它和線程一樣可以調度,但是不同的是線程的啟動和調度需要通過操作系統來處理。
協程是一種比線程更加輕量級的存在,最重要的是,協程不被操作系統內核管理,協程是完全由程序控制的。
運行效率極高,協程的切換完全由程序控制,不像線程切換需要花費操作系統的開銷,線程數量越多,協程的優勢就越明顯。
協程不需要多線程的鎖機制,因為只有一個線程,不存在變量衝突。
對於多核CPU,利用多進程+協程的方式,能充分利用CPU,獲得極高的性能。
注意協程這個概念完全是程序員自己想出來的東西,它對於操作系統來說根本不存在。操作系統只有進程和線程。
Python中使用協程的例子
yield
關鍵字相當於是暫停功能,程序運行到yield
停止,send
函數可以傳參給生成器函數,參數賦值給yield
。
def customer():
while True:
number = yield
print('開始消費:',number)
custom = customer()
next(custom)
for i in range(5):
print('開始生產:',i)
custom.send(i)
結果如下
開始生產: 0
開始消費: 0
開始生產: 1
開始消費: 1
開始生產: 2
開始消費: 2
開始生產: 3
開始消費: 3
開始生產: 4
開始消費: 4
代碼解析:
- 協程使用生成器函數定義:定義體中有 yield 關鍵字。
- yield 在表達式中使用;如果協程只需從客戶custom接收數據,如果沒有產出的值,那麼產出的值是 None。
- 首先要調用 next(…) 函數,因為生成器還沒啟動,沒在 yield 語句處暫停,所以一開始無法發送數據。
- 調用send方法,把值傳給 yield 的變量,然後協程恢復,繼續執行下面的代碼,直到運行到下一個 yield 表達式,或者終止。
async和await
async和await是原生協程,是Python3.5以後引入的兩個關鍵詞。
async
是「異步」的簡寫,而 await
可以認為是 async wait
的簡寫。所以應該很好理解 async
用於申明一個 function
是異步的,而 await
用於等待一個異步方法執行完成。
下面,我們從一個demo示例看起,具體代碼如下。
import time
def print_num(num):
print("Maoli is printing " + str(num) + " nows" )
time.sleep(1)
print("Maoli prints" + str(num) + " OK")
def main(nums):
for num in nums:
print_num(num)
%time main([i for i in range(1,6)])
Maoli is printing 1 nows
Maoli prints1 OK
Maoli is printing 2 nows
Maoli prints2 OK
Maoli is printing 3 nows
Maoli prints3 OK
Maoli is printing 4 nows
Maoli prints4 OK
Maoli is printing 5 nows
Maoli prints5 OK
Wall time: 5 s
%time 需要在jupyter notebook中運行,這是jupyter的語法糖。
上面代碼是從上到下執行的。下面我們將上面的代碼改成單線程協程版本。
注意py版本3.7以上,主要使用的是asyncio模塊,如果出現AttributeError: module 『asyncio『 has no attribute 『run『
報錯,這是asyncio版本不兼容的原因,需要將Python版本提升至3.7以上。
import asyncio
async def print_num(num):
print("Maoli is printing " + str(num) + " nows" )
await asyncio.sleep(1)
print("Maoli prints" + str(num) + " OK")
async def main(nums):
for num in nums:
await print_num(num)
%time asyncio.run(main([i for i in range(1,6)]))
Maoli is printing 1 nows
Maoli prints1 OK
Maoli is printing 2 nows
Maoli prints2 OK
Maoli is printing 3 nows
Maoli prints3 OK
Maoli is printing 4 nows
Maoli prints4 OK
Maoli is printing 5 nows
Maoli prints5 OK
Wall time: 5.01 s
asyncio.run() 函數用來運行最高層級的入口點 “main()” 函數。await 是同步調用等待一個協程。以下代碼段會在等待 1 秒後打印 num,但在運行速度上沒有發生改變。這裡需要引入asyncio.create_task
可等待對象才可以。
create_task
如果一個對象可以在 await
語句中使用,那麼它就是可等待對象。
協程中的還一個重要概念,任務(Task)。
如果寫一個數字是一個任務,那麼毛利我要完成5個任務。
毛利我寫個1-5都這麼慢,不行,我要加速寫。
asyncio.create_task()
函數用來並發運行作為 asyncio
任務 的多個協程。
import asyncio
async def print_num(num):
print("Maoli is printing " + str(num) + " nows" )
await asyncio.sleep(1)
print("Maoli prints" + str(num) + " OK")
async def main(nums):
tasks = [asyncio.create_task(print_num(num)) for num in nums]
for task in tasks:
await task
%time asyncio.run(main([i for i in range(1,6)]))
Maoli is printing 1 nows
Maoli is printing 2 nows
Maoli is printing 3 nows
Maoli is printing 4 nows
Maoli is printing 5 nows
Maoli prints1 OK
Maoli prints3 OK
Maoli prints5 OK
Maoli prints2 OK
Maoli prints4 OK
Wall time: 1.01 s
還可以寫成await asyncio.gather(*tasks)
這種方法
import asyncio
async def print_num(num):
print("Maoli is printing " + str(num) + " nows" )
await asyncio.sleep(1)
print("Maoli prints" + str(num) + " OK")
async def main(nums):
tasks = [asyncio.create_task(print_num(num)) for num in nums]
await asyncio.gather(*tasks)
%time asyncio.run(main([i for i in range(1,6)]))
*tasks
解包列表,將列表變成了函數的參數;與之對應的是, ** dict
將字典變成了函數的參數。
協程的寫法簡潔清晰,只要把 async / await 語法和 create_task 結合來用,就是Python中比較常見的協程寫法。
今天也學到了很多東西呢,明天有什麼新知識呢?真期待鴨如果喜歡文章可以關注我哦
//docs.python.org/zh-cn/3/library/asyncio.html
本文已收錄 GitHub,傳送門~ ,裏面更有大廠面試完整考點,歡迎 Star。