-
啟動一個無返回值協程
通過async關鍵字定義一個協程
import sys import asyncio async def coroutine(): print('執行協程') if sys.version_info >= (3, 7, 0): asyncio.run(coroutine()) else: loop = asyncio.get_event_loop() loop.run_until_complete(coroutine()) loop.close()
輸出結果如下
執行協程
-
啟動一個有返回值協程
import sys import asyncio async def coroutine(): print('執行協程') return 'done!' if sys.version_info >= (3, 7, 0): result = asyncio.run(coroutine()) else: loop = asyncio.get_event_loop() result = loop.run_until_complete(coroutine()) loop.close() print(result)
輸出結果如下
執行協程 done!
-
使用await關鍵字
await關鍵字只能在一個協程內部使用
await等待另一個協程任務結果,它不會阻塞事件迴圈,但會在阻塞當前協程的上下文
import sys import asyncio async def coroutine(): print('執行協程 排程asyncio.sleep協程任務') await asyncio.sleep(2) return 'done!' print('啟動時間:', time.time()) if sys.version_info >= (3, 7, 0): result = asyncio.run(coroutine()) else: loop = asyncio.get_event_loop() result = loop.run_until_complete(coroutine()) loop.close() print(result) print('結束時間:', time.time())
輸出結果如下
啟動時間: 1598236154.90588 執行協程 排程asyncio.sleep協程任務 done! 結束時間: 1598236156.9118028
-
回撥函式: 迅速回撥
迅速回撥是立即主動呼叫一個方法
回撥方法必然是一個普通函式
回撥通過事件迴圈loop提供的方法新增,它不會阻塞協程上下文
同一個協程內的回撥函式之間存在阻塞關係,不同協程內的回撥函式之間無阻塞關係
import sys import asyncio def callback(a, c=2): time.sleep(c) print(f"傳入引數a={a}, c={c}, 時間{time.time()}") async def main(): print(f'新增回撥, 時間{time.time()}') loop = asyncio.get_running_loop() loop.call_soon(callback, 1) loop.call_soon(callback, 2, 0) print(f'協程結束, 時間{time.time()}') if sys.version_info >= (3, 7, 0): result = asyncio.run(main()) else: loop = asyncio.get_event_loop() result = loop.run_until_complete(main()) loop.close()
輸出結果如下
新增回撥, 時間1598236958.119624 協程結束, 時間1598236958.1196742 傳入引數a=1, c=2, 時間1598236960.1198878 傳入引數a=2, c=0, 時間1598236960.119946
-
回撥函式:延時回撥
延時回撥可以指定在多久後執行回撥函式
延時回撥的其他特性和迅速回撥一致
特別注意,如果事件迴圈內的協程已經執行結束,尚在等待呼叫的回撥函式不會被呼叫
import sys import asyncio def callback(a, c=3): time.sleep(c) print(f"傳入引數a={a}, c={c}, 時間{time.time()}") async def main(): print(f'新增回撥, 時間{time.time()}') loop = asyncio.get_running_loop() loop.call_later(1, callback, 2, 0) loop.call_soon(callback, 1) print(f'協程結束, 時間{time.time()}') if sys.version_info >= (3, 7, 0): result = asyncio.run(main()) else: loop = asyncio.get_event_loop() result = loop.run_until_complete(main()) loop.close()
輸出結果如下
新增回撥, 時間1598237313.375363 協程結束, 時間1598237313.375415 傳入引數a=1, c=3, 時間1598237316.376031 傳入引數a=2, c=0, 時間1598237316.376389
解釋說明,對於協程main,call_later和call_soon是不會阻塞協程上下文的,因此兩個回撥函式可以視為同時加入了事件迴圈的回撥,其中回撥callback(1)方法是立即呼叫,回撥函式會sleep 3秒,而回撥方法callback(2, 0)是在協程延時 1秒後呼叫,但回撥函式之間是存在阻塞關係的,因此它會等待callback(1)先執行結束,然後判斷是否滿足了延時條件再執行。
-
回撥函式:定時回撥
此處的指定時間並非系統時間戳,而是指事件迴圈建立的時間戳
通過loop.time()獲取事件迴圈時間戳
import sys import asyncio def callback(a, loop, c=3): time.sleep(c) print(f"傳入引數a={a}, c={c}, 時間{loop.time()}") async def main(): loop = asyncio.get_running_loop() now = loop.time() print(f'事件迴圈時間戳{now}') loop.call_at(now + 4, callback, 2, loop, 1) loop.call_soon(callback, 1, loop) await asyncio.sleep(5) print(f'協程結束, 時間戳{loop.time()}') if sys.version_info >= (3, 7, 0): result = asyncio.run(main()) else: loop = asyncio.get_event_loop() result = loop.run_until_complete(main()) loop.close()
-
asyncio.Future物件
Future的例項化物件可以認為是一個協程物件,它可以使用await關鍵字
通過延時回撥+Future可以模擬一個協程的邏輯
即延時回撥 ≈ 非同步等待返回結果
Future ≈ 非阻塞模型,它不會阻塞事件迴圈的其他協程
import sys import asyncio def mark_done(future: asyncio.Future, result): print(f'標記future結束 時間{time.time()}') future.set_result(result) async def main(): print(f'協程開始 時間{time.time()}') loop = asyncio.get_running_loop() future = asyncio.Future() loop.call_later(3, mark_done, future, 'done!') result = await future print(f'future結果 {result} 時間{time.time()}') if sys.version_info >= (3, 7, 0): result = asyncio.run(main()) else: loop = asyncio.get_event_loop() result = loop.run_until_complete(main()) loop.close()
輸出結果如下
協程開始 時間1598238858.6768472 標記future結束 時間1598238861.678293 future結果 done! 時間1598238861.678438
-
Future回撥
Future的回撥是在一個Future物件標記結束後執行的函式
await會等待回撥函式執行結束,即回撥函式會在協程上下文阻塞
import sys import asyncio def callback(future: asyncio.Future): # 基於Future的回撥函式一定要接受一個Future物件 print(f'Future回撥被呼叫 時間{time.time()}') time.sleep(1) async def main(): print(f'協程開始 時間{time.time()}') loop = asyncio.get_running_loop() future = asyncio.Future() future.add_done_callback(callback) loop.call_later(1, lambda future: future.set_result('done'), future) result = await future print(f'future結果 {result} 時間{time.time()}') if sys.version_info >= (3, 7, 0): result = asyncio.run(main()) else: loop = asyncio.get_event_loop() result = loop.run_until_complete(main()) loop.close()
輸出結果如下
協程開始 時間1598241100.534262 Future回撥被呼叫 時間1598241101.539032 future結果 done 時間1598241102.5411909
-
aysncio.Task物件
Task物件在呼叫create_task會立即執行,它類似迅速回撥
asyncio.create_task是python3.7引入的高階api,3.7以下使用ensure_future方法
與回撥函式的區別:
- 回撥函式一定不是協程,而Task物件只能建立協程任務
- 回撥函式的傳入引數直接通過新增回撥的方法傳入,而任務物件直接傳入協程形參
- 回撥函式之間執行是阻塞的,而Task物件則是基於事件迴圈的標準協程
在協程上下文中可以使用await關鍵字等待任務結果
import sys import asyncio async def task_func(n): print(f'執行 task_func 時間{time.time()}') await asyncio.sleep(n) return 'task done!' async def main(): loop = asyncio.get_running_loop() task = loop.create_task(task_func(3)) result = await task print(f'task結果 {result} 時間{time.time()}') if sys.version_info >= (3, 7, 0): result = asyncio.run(main()) else: loop = asyncio.get_event_loop() result = loop.run_until_complete(main()) loop.close()
輸出結果如下
執行 task_func 時間1598241581.334356 task結果 task done! 時間1598241584.338337
-
Task取消任務
可以取消一個正在事件迴圈內執行的task物件
Task和Future一樣,支援通過add_done_callback新增回撥函式
import sys import asyncio async def task_func(n): print(f'執行 task_func 時間{time.time()}') await asyncio.sleep(n) return 'task done!' async def main(): loop = asyncio.get_running_loop() task = asyncio.create_task(task_func(5)) loop.call_later(3, lambda task: task.cancel(), task) # Task被取消 會丟擲CancelledError異常 try: result = await task print(f'task結果 {result} 時間{time.time()}') except asyncio.exceptions.CancelledError: print(f'task被取消了 時間{time.time()}') if sys.version_info >= (3, 7, 0): result = asyncio.run(main()) else: loop = asyncio.get_event_loop() result = loop.run_until_complete(main()) loop.close()
輸出結果如下
執行 task_func 時間1598242256.3299708 task被取消了 時間1598242259.335436
-
使用asyncio.ensure_future建立任務
asyncio.ensure_future實現效果和asyncio.create_task一致
import sys import asyncio async def task_func(n): print(f'執行 task_func 時間{time.time()}') await asyncio.sleep(n) return 'task done!' async def main(): print(f'協程開始 時間{time.time()}') task = asyncio.ensure_future(task_func(2)) result = await task print(f'task結果 {result} 時間{time.time()}') if sys.version_info >= (3, 7, 0): result = asyncio.run(main()) else: loop = asyncio.get_event_loop() result = loop.run_until_complete(main()) loop.close()
輸出結果如下
協程開始 時間1598249260.1346428 執行 task_func 時間1598249260.1347158 task結果 task done! 時間1598249262.137878
-
asyncio.wait執行多個協程
asyncio.wait接受一個協程列表/元組將其加入事件迴圈
返回兩個列表,分別包含已完成和正在執行的Future物件
asyncio.wait 會阻塞協程上下文直至滿足指定的條件(預設條件所有協程執行結束)
wait支援設定一個超時時間,但在超時發生時不會取消可等待物件,但若事件迴圈結束時未完成則會丟擲CancelledError異常。如果要超時主動取消,可用wait_for方法
asyncio.wait 返回的結果集是按照事件迴圈中的任務完成順序排列的,所以通常和原始任務順序不同
import sys import asyncio async def corn_sleep(n): try: await asyncio.sleep(n) except asyncio.exceptions.CancelledError: print(f'corn({n})超時取消! 時間{time.time()}') print(f'corn({n}) done! 時間{time.time()}') return f'corn({n}) done! 時間{time.time()}' async def main(): print(f'協程開始 時間{time.time()}') tasks = [corn_sleep(i) for i in range(1, 6)] done, pending = await asyncio.wait(tasks, timeout=3) for task in done: pass # print(task.result()) for task in pending: print(task.done()) await asyncio.sleep(1) print(f'協程結束 時間{time.time()}') if sys.version_info >= (3, 7, 0): result = asyncio.run(main()) else: loop = asyncio.get_event_loop() result = loop.run_until_complete(main()) loop.close()
輸出結果如下
協程開始 時間1598253388.614105 corn(1) done! 時間1598253389.6196852 corn(2) done! 時間1598253390.6171541 False False False corn(3) done! 時間1598253391.617078 corn(4) done! 時間1598253392.617147 協程結束 時間1598253392.617195 corn(5)超時取消! 時間1598253392.6173718 corn(5) done! 時間1598253392.6173818
-
asyncio.gather執行多個協程
gather方法和wait都可以執行多個協程,但輸入和輸出有所差異
- 輸出差異,gather保證了結果列表的順序,它是嚴格遵循傳入任務順序的
- 輸入差異,wait方法接受的是一個協程列表,而gather是通過可變長引數傳入協程方法的
- wait支援超時設定,gather無法設定超時時間
- wait返回兩個列表,列表元素是Future物件,gather只返回done列表,列表元素是Future物件的result()結果
import sys import asyncio async def corn_sleep(n): await asyncio.sleep(n) return f'corn({n}) done! 時間{time.time()}' async def main(): print(f'協程開始 時間{time.time()}') tasks = [corn_sleep(i) for i in range(1, 6)] done = await asyncio.gather(*tasks) for result in done: print(result) print(f'協程結束 時間{time.time()}') if sys.version_info >= (3, 7, 0): result = asyncio.run(main()) else: loop = asyncio.get_event_loop() result = loop.run_until_complete(main()) loop.close()
輸出結果如下
協程開始 時間1598250547.5619462 corn(1) done! 時間1598250548.567125 corn(2) done! 時間1598250549.5660028 corn(3) done! 時間1598250550.563731 corn(4) done! 時間1598250551.566098 corn(5) done! 時間1598250552.566897 協程結束 時間1598250552.566983
-
asyncio.as_completed執行多個協程
as_completed方法功能同wait和gather都可以用於執行多個協程
as_completed接受的是協程列表,返回的是一個迭代器,迭代元素為Future物件
as_completed方法支援超時設定,但它會在協程上下文丟擲asyncio.exceptions.TimeoutError錯誤
as_completed方法返回結果集是無序的
import sys import asyncio async def corn_sleep(n): await asyncio.sleep(n) return f'corn({n}) done! 時間{time.time()}' async def main(): print(f'協程開始 時間{time.time()}') tasks = [corn_sleep(i) for i in range(1, 6)] for task in asyncio.as_completed(tasks, timeout=3.5): try: result = await task print(result) except asyncio.exceptions.TimeoutError: print(f'協程超時了 時間{time.time()}') break print(f'協程結束 時間{time.time()}') if sys.version_info >= (3, 7, 0): result = asyncio.run(main()) else: loop = asyncio.get_event_loop() result = loop.run_until_complete(main()) loop.close()
輸出結果如下
協程開始 時間1598251179.489662 corn(1) done! 時間1598251180.493866 corn(2) done! 時間1598251181.4911182 corn(3) done! 時間1598251182.493823 協程超時了 時間1598251182.991565 協程結束 時間1598251182.991592
-
協程鎖
相比執行緒、程式鎖,協程鎖似乎應用場景不那麼多,比如儲存檔案時?
import sys import asyncio from functools import partial def callback(lock: asyncio.Lock): print(f'釋放鎖 時間{time.time()}') lock.release() async def corn1(x, loop, lock): async with lock: pass # 加鎖,利用延遲迴調1秒後解鎖 await lock.acquire() loop.call_later(1, callback, lock) return f'{x} done! 時間{time.time()}' async def main(): loop = asyncio.get_running_loop() lock = asyncio.Lock() await lock.acquire() print(f'加鎖 時間{time.time()}') loop.call_later(2, callback, lock) corn = partial(corn1, loop=loop, lock=lock) results = await asyncio.gather(corn(1), corn(2), corn(3)) for result in results: print(result) if sys.version_info >= (3, 7, 0): result = asyncio.run(main()) else: loop = asyncio.get_event_loop() result = loop.run_until_complete(main()) loop.close()
輸出結果如下
加鎖 時間1598251855.600318 釋放鎖 時間1598251857.6041799 釋放鎖 時間1598251858.606617 釋放鎖 時間1598251859.609798 1 done! 時間1598251857.604349 2 done! 時間1598251858.606834 3 done! 時間1598251859.6099358
-
Event 事件物件
Event物件和Lock物件有一定的相似性,可用於同步操作
Event物件提供一個狀態標記位,它是一個布林值,只用來判斷狀態
import sys import asyncio def task_done(event: asyncio.Event): event.set() async def corn(event: asyncio.Event): await event.wait() print(f'corn done! 時間{time.time()}') async def main(): print(f'協程開始 時間{time.time()}') event = asyncio.Event() loop = asyncio.get_running_loop() loop.call_later(2, task_done, event) await corn(event) if sys.version_info >= (3, 7, 0): result = asyncio.run(main()) else: loop = asyncio.get_event_loop() result = loop.run_until_complete(main()) loop.close()
輸出結果如下
協程開始 時間1598252539.2691798 corn done! 時間1598252541.273142
-
Queue 協程佇列
與普通佇列相比,協程佇列有一個task_done方法,用於標記某一個任務結束,而join方法則會阻塞,直到滿足從佇列取出的個數等於task_done方法呼叫的次數
import sys import asyncio async def producer(queue: asyncio.Queue): print('生產者 上班') for i in range(1, 6): await queue.put(f'產品({i})') await asyncio.sleep(2) await queue.put(None) await queue.join() print('生產者 打烊') async def consumer(x, queue: asyncio.Queue): print(f'消費者{x}號進場') while True: pt = await queue.get() queue.task_done() if pt is None: await queue.put(None) break else: print(f'{x}號 消費了 {pt}') async def main(): loop = asyncio.get_running_loop() queue = asyncio.Queue(maxsize=3) loop.create_task(producer(queue)) await asyncio.wait([consumer(i, queue) for i in range(3)]) if sys.version_info >= (3, 7, 0): result = asyncio.run(main()) else: loop = asyncio.get_event_loop() result = loop.run_until_complete(main()) loop.close()
輸出結果如下
生產者 上班 消費者2號進場 2號 消費了 產品(1) 消費者0號進場 消費者1號進場 2號 消費了 產品(2) 0號 消費了 產品(3) 1號 消費了 產品(4) 2號 消費了 產品(5) 生產者 下班
-
asyncio.subprocess 非同步呼叫子程式
asyncio提供create_subprocess_exec和create_subprocess_shell方法
前者可以呼叫任意程式,後者則通過shell命令列呼叫其他程式
import sys import asyncio async def get_date(): code = 'import datetime; print(datetime.datetime.now())' task1 = asyncio.create_subprocess_exec( sys.executable, '-c', code, stdout=asyncio.subprocess.PIPE ) task2 = asyncio.create_subprocess_shell( 'ls -all ~/Desktop', stdout=asyncio.subprocess.PIPE ) results = await asyncio.gather(task1, task2) response = [] for proc in results: data = await proc.stdout.read() line = data.decode('utf8').rstrip() response.append(line) return '\n'.join(response) date = asyncio.run(get_date()) print(f"當前時間: {date}")
輸出結果如下
當前時間: 2020-08-24 16:11:18.143943 total 16 drwxr-xr-x 3 sw staff 96 Apr 6 09:11 $RECYCLE.BIN drwx------@ 8 sw staff 256 Jun 18 10:52 . drwxr-xr-x+ 63 sw staff 2016 Aug 23 20:52 .. -rw-r--r--@ 1 sw staff 6148 Jul 22 17:54 .DS_Store -rw-r--r-- 1 sw staff 0 May 27 2019 .localized drwxr-xr-x 3 sw staff 96 Feb 3 2020 Don't Starve Together.app drwxr-xr-x 9 sw staff 288 May 3 18:24 HeavenMS drwxr-xr-x 3 sw staff 96 Sep 20 2019 Tomb Raider.app
-
網路通訊-高階API
asyncio封裝了幾個高階方法來快速實現網路通訊
- await asyncio.open_connection() 建立TCP連線
- await asyncio.open_unix_connection() 建立Unix socket連線
- await start_server() 啟動TCP服務
- await start_unix_server() 啟動Unix socket服務
- StreamReader 接收網路資料的高階async/await物件
- StreamWriter 傳送網路資料的高階async/await物件
以搭建一個簡單http伺服器為例
import sys import asyncio async def http_handle(render: asyncio.StreamReader, writer: asyncio.WriteTransport): """在asyncio.start_server傳入該協程 當一個tcp連線建立時就會呼叫該回撥,並傳入兩個引數 :param render: StreamReader物件 :param writer: StreamWriter物件 StreamReader和StreamWriter物件都是繼承於Transport :return: """ message = await render.read(1024) print(message.decode()) writer.write(b'HTTP/1.1 200 OK\r\n\r\nHello Client!') writer.close() async def main(): server = await asyncio.start_server(http_handle, '127.0.0.1', 8000) addr = server.sockets[0].getsockname() print(f'伺服器啟動 {addr}') async with server: await server.serve_forever() if sys.version_info >= (3, 7, 0): result = asyncio.run(main()) else: loop = asyncio.get_event_loop() result = loop.run_until_complete(main()) loop.close()
瀏覽器開啟http://127.0.0.1:8000 控制檯輸出結果如下
GET / HTTP/1.1 Host: 127.0.0.1:8000 Connection: keep-alive Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9
瀏覽器顯示結果
Hello Client!
-
網路通訊-低階API
案例19所用的方法是基於底層Transport和Protocol封裝的結果
Protocol協議類是通過重寫一系列回撥方法來處理網路請求
Transport例項可以簡單看作是對一個socket物件封裝,實現了資料收發功能
除了網路通訊外,子程式(程式通訊)也是通過協議回撥來實現
以一個tcp服務端為例,較常用的有4個回撥函式
- connection_made() 連線建立時被呼叫
- connection_lost() 連線丟失或關閉時被呼叫
- data_received() 接收到資料時被呼叫
- eof_received() 接收到EOF時被呼叫
以案例19的http服務端為例,實現相同效果
import sys import asyncio class HttpProtocol(asyncio.Protocol): def __init__(self): """每個tcp連線建立時都會例項化這個類""" self.transport: asyncio.Transport = None def connection_made(self, transport: asyncio.Transport): """連線建立時被呼叫""" self.transport = transport def data_received(self, data): """接收資料時被呼叫""" print(data.decode()) self.transport.write(b'HTTP/1.1 200 OK\r\n\r\nHello Client!') self.transport.close() loop = asyncio.get_event_loop() server = loop.create_server(HttpProtocol, '127.0.0.1', 8000) loop.run_until_complete(server) loop.run_forever()
-
訊號處理
事件迴圈能夠支援新增訊號來執行回撥函式
import signal import asyncio def ask_exit(sig_name: str, loop: asyncio.AbstractEventLoop): print("捕獲訊號 %s: exit" % sig_name) loop.stop() async def main(): loop = asyncio.get_running_loop() for signame in {'SIGINT', 'SIGTERM'}: loop.add_signal_handler( getattr(signal, signame), partial(ask_exit, signame, loop)) await asyncio.sleep(3600) print("事件迴圈將在1小時後或者按下Ctrl+C停止") try: asyncio.run(main()) except RuntimeError as e: if str(e) != 'Event loop stopped before Future completed.': raise
輸出結果如下
事件迴圈將在1小時後或者按下Ctrl+C停止 ^C捕獲訊號 SIGINT: exit
高頻方法總結
-
回撥和Task具有一定相似性,回撥是函式,任務是協程
-
向事件迴圈非阻塞執行一個協程是建立一個任務,可以使用如下方法
方法 傳入 返回 備註 asyncio.create_task 協程 Task物件 高層級api、Python3.7 + asyncio.ensure_future 協程 Task物件 高層級api loop.create_task 協程 Task物件 低層級api -
向事件迴圈新增多個協程(使用await關鍵詞才會實際新增執行),可使用如下方法
方法 傳入 返回 備註 asyncio.wait 協程列表 done/pending列表
元素是Future物件支援超時設定,結果無序 asyncio.gather 變長引數 協程返回值組成的列表 返回結果列表有序 asyncio.as_completed 協程列表 協程任務迭代器 通過for迴圈遍歷獲取任務結果 -
在unix平臺,可以使用效能更好uvloop來替代asyncio預設的事件迴圈
import uvloop asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())