asyncio
aysncio是Python3.4版本引入的標准庫,直接內置了對異步IO的支持。
asyncio的編程模式就是一個消息循環。我們從asyncio模板中直接獲取一個Eventloop(事件循環)的引用,然后把需要執行的協程扔到Eventloop中執行,就實現了異步IO。
用asyncio實現Hello world代碼如下
async_hello.py
# asyncio start import asyncio # 裝飾器裝飾的函數為協程函數 @asyncio.coroutine def hello(): print('Hello world!') r = yield from asyncio.sleep(1) print('Hello again!') # 獲取Eventloop事件循環 loop = asyncio.get_event_loop() # 執行協程,協程函數不能直接執行,需要放入事件循環才能執行 loop.run_until_complete(hello()) # 關閉事件循環 loop.close() # asyncio end
輸出如下
Hello world! Hello again!
解析:
運行事件循環,事件循環綁定的協程函數是hello()該協程函數首先打印 “Hello world! ” 然后遇到yield from asyncio.sleep(1)模擬的IO耗時操作,此時如果事件循環綁定了其他協程則會去執行其他協程,本次只綁定了一個協程所以等待1秒后輸出“hello again!”
@asyncio.coroutine
把一個generator標記為coroutine類型,然后,我們就把這個coroutine
扔到EventLoop
中執行。
hello()
會首先打印出Hello world!
,然后,yield from
語法可以讓我們方便地調用另一個generator
。由於asyncio.sleep()
也是一個coroutine
,所以線程不會等待asyncio.sleep()
,而是直接中斷並執行下一個消息循環。當asyncio.sleep()
返回時,線程就可以從yield from
拿到返回值(此處是None
),然后接着執行下一行語句。
把asyncio.sleep(1)
看成是一個耗時1秒的IO操作,在此期間,主線程並未等待,而是去執行EventLoop
中其他可以執行的coroutine
了,因此可以實現並發執行。
我們用Task封裝兩個coroutine
試試:
# 封裝兩個task任務 start import asyncio import threading # 裝飾器裝飾的函數為協程函數 @asyncio.coroutine def hello(): print('Hello world! %s' % threading.currentThread() ) yield from asyncio.sleep(1) print('Hello again! %s' % threading.currentThread()) # 獲取Eventloop事件循環 loop = asyncio.get_event_loop() # 執行協程,協程函數不能直接執行,需要放入事件循環才能執行 tasks = [hello(),hello()] loop.run_until_complete(asyncio.wait(tasks)) # 關閉事件循環 loop.close() # 封裝兩個task任務 end
輸出如下
Hello world! <_MainThread(MainThread, started 18168)> Hello world! <_MainThread(MainThread, started 18168)> 等待約1秒 Hello again! <_MainThread(MainThread, started 18168)> Hello again! <_MainThread(MainThread, started 18168)>
由打印的當前線程名稱可以看出,兩個coroutine是由同一個線程並發執行的。所以輸出兩個Hello world!幾乎是同時的,然后兩個協程同時等待1秒輸出兩個Hello again! 。如果不是並發則應該前后兩次輸出,類似下面這樣
Hello world! <_MainThread(MainThread, started 18168)> 等待約1秒 Hello again! <_MainThread(MainThread, started 18168)> Hello world! <_MainThread(MainThread, started 18168)> 等待約1秒 Hello again! <_MainThread(MainThread, started 18168)>
如果把asyncio.sleep()換成真正的IO操作,則多個coroutine是由同一個線程並發執行的。
我們用asyncio的異步網絡連接來獲取sina,sohu和163的網站首頁
async_wget.py
import asyncio @asyncio.coroutine def wget(host): print('wget %s...' % host) # 創建TCP客戶端並連接服務器,或者說創建一個TCP連接對象 # open_connection接收兩個參數:主機名和端口號 # connect是協程,這步僅創建協程對象,立即返回,不阻塞 connect = asyncio.open_connection(host,80) # 運行協程連接服務器,這步是阻塞操作,釋放CPU # 連接創建成功后,asyncio.open_connection方法返回的就是讀寫對象 # 讀寫對象分別為 StreamReader和StreamWriter實例 # 它們也是協程對象,底層調用Socker模塊的send和recv方法實現讀寫 reader,writer = yield from connect # heade是發送給服務器的消息,意為獲取頁面的header信息 # 它的格式是固定的 header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' %host # 給服務器發消息,注意消息是二進制的,本次使用utf-8編碼 writer.write(header.encode('utf-8')) # writer.drain()是一個與底層IO輸入緩沖區交互流量控制方法,需要配合writer.write使用 # 當緩沖區達到上限時,drain()阻塞,待緩沖區回落到下限時,寫操作恢復 # 當不需要等待時,drain()會立即返回,假如上面的消息內容較少,不會阻塞 # 這就是一個控制消息數據量的控制閥 yield from writer.drain() # 給服務器發送消息后,就等着讀取服務器返回來的消息 while True: # 讀取數據是阻塞操作,釋放CPU # reader相當於一個水盆,服務器發來的數據是水流 # readline表示讀取一行,以\n作為換行符 # 如果在出現\n之前,數據流出現EOF(End of File文件結束符)也會返回 # 相當於出現\n或EOF時,擰上水龍頭,line就是這盆水 line = yield from reader.readline() # 數據接收完畢,會返回空字符串\r\n,退出while循環,結束數據接收 if line == b'\r\n': break # 接收數據是二進制,轉換為UTF-8格式並打印 # rstrip()方法刪掉字符串結尾就是右邊的空白字符,也就是\n print('%s header > %s' % (host,line.decode('utf-8').rstrip())) # 關閉數據流,可以省略 writer.close() yield from writer.wait_closed() hosts = ['192.168.1.100'] hosts = ['www.sina.com.cn','www.sohu.com','www.163.com'] # 創建task任務 tasks = [wget(host) for host in hosts ] # 創建事件循環 loop = asyncio.get_event_loop() # 運行事件循環 loop.run_until_complete(asyncio.wait(tasks))
執行結果如下
wget www.sohu.com... wget www.163.com... wget www.baidu.com... www.sohu.com header > HTTP/1.1 307 Temporary Redirect www.sohu.com header > Content-Type: text/html www.sohu.com header > Content-Length: 180 www.sohu.com header > Connection: close www.sohu.com header > Server: nginx www.sohu.com header > Date: Fri, 29 Oct 2021 09:18:16 GMT www.sohu.com header > Location: https://www.sohu.com/ www.sohu.com header > FSS-Cache: from 4094608.6191770.5431472 www.sohu.com header > FSS-Proxy: Powered by 4356756.6716062.5693624 www.163.com header > HTTP/1.1 301 Moved Permanently www.163.com header > Date: Fri, 29 Oct 2021 09:18:16 GMT www.163.com header > Content-Length: 0 www.163.com header > Connection: close www.163.com header > Server: web cache www.163.com header > Location: https://www.163.com/ www.163.com header > Cache-Control: no-cache,no-store,private www.163.com header > cdn-user-ip: 116.25.237.202 www.163.com header > cdn-ip: 183.47.233.150 www.163.com header > X-Cache-Remote: MISS www.163.com header > cdn-source: baishan www.baidu.com header > HTTP/1.0 200 OK www.baidu.com header > Accept-Ranges: bytes www.baidu.com header > Cache-Control: no-cache www.baidu.com header > Content-Length: 14615 www.baidu.com header > Content-Type: text/html www.baidu.com header > Date: Fri, 29 Oct 2021 09:18:16 GMT www.baidu.com header > P3p: CP=" OTI DSP COR IVA OUR IND COM " www.baidu.com header > Pragma: no-cache www.baidu.com header > Server: BWS/1.1 www.baidu.com header > Set-Cookie: BAIDUID=00D297D9A818428E9A332BB01C2BA52E:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com www.baidu.com header > Set-Cookie: BIDUPSID=00D297D9A818428E9A332BB01C2BA52E; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com www.baidu.com header > Set-Cookie: PSTM=1635499096; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com www.baidu.com header > Set-Cookie: BAIDUID=00D297D9A818428E1A7073DA9DEB891D:FG=1; max-age=31536000; expires=Sat, 29-Oct-22 09:18:16 GMT; domain=.baidu.com; path=/; version=1; comment=bd www.baidu.com header > Traceid: 1635499096061364455412067686187092680246 www.baidu.com header > Vary: Accept-Encoding www.baidu.com header > X-Frame-Options: sameorigin www.baidu.com header > X-Ua-Compatible: IE=Edge,chrome=1 PS D:\learn-python3\函數式編程> & C:/ProgramData/Anaconda3/python.exe d:/learn-python3/異步IO/async_wget.py d:/learn-python3/異步IO/async_wget.py:4: DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead def wget(host): wget www.163.com... wget www.sohu.com... wget www.sina.com.cn... www.sohu.com header > HTTP/1.1 307 Temporary Redirect www.sohu.com header > Content-Type: text/html www.sohu.com header > Content-Length: 180 www.sohu.com header > Connection: close www.sohu.com header > Server: nginx www.sohu.com header > Date: Fri, 29 Oct 2021 09:19:33 GMT www.sohu.com header > Location: https://www.sohu.com/ www.sohu.com header > FSS-Cache: from 4160145.6322843.5497010 www.sohu.com header > FSS-Proxy: Powered by 4356756.6716062.5693624 www.163.com header > HTTP/1.1 301 Moved Permanently www.163.com header > Date: Fri, 29 Oct 2021 09:19:33 GMT www.163.com header > Content-Length: 0 www.163.com header > Connection: close www.163.com header > Server: web cache www.163.com header > Location: https://www.163.com/ www.163.com header > Cache-Control: no-cache,no-store,private www.163.com header > cdn-user-ip: 116.25.237.202 www.163.com header > cdn-ip: 183.47.233.148 www.163.com header > X-Cache-Remote: MISS www.163.com header > cdn-source: baishan www.sina.com.cn header > HTTP/1.1 302 Found www.sina.com.cn header > Server: Tengine www.sina.com.cn header > Date: Fri, 29 Oct 2021 09:19:33 GMT www.sina.com.cn header > Content-Type: text/html www.sina.com.cn header > Content-Length: 242 www.sina.com.cn header > Connection: close www.sina.com.cn header > Location: https://www.sina.com.cn/ www.sina.com.cn header > X-DSL-CHECK: 5 www.sina.com.cn header > X-Via-CDN: f=alicdn,s=cache5.cn1366,c=116.25.237.202; www.sina.com.cn header > Via: cache5.cn1366[,0] www.sina.com.cn header > Timing-Allow-Origin: * www.sina.com.cn header > EagleId: 0e1d289916354991731167720e
可見3個連接由一個線程通過coroutine並發完成。
小結
asyncio
提供了完善的異步IO支持;
異步操作需要在coroutine
中通過yield from
完成;
多個coroutine
可以封裝成一組Task然后並發執行。