pythonasyncio在哪个版本好_理解Python asyncio的简洁方式
異步IO是個好東西,在網絡讀寫場景中可以大大提高程序的并發能力,比如爬蟲、web服務等。這樣的好東西自然也要在Python中可以使用。不過,在漫長的Python2時代,官方并沒有推出一個自己的異步IO庫,到了Python 3.4 才推出。我們先來看看異步IO在Python中的發展歷史。
Python 異步IO的歷史
Python 2的異步IO庫
Python 2 時代官方并沒有異步IO的支持,但是有幾個第三方庫通過事件或事件循環(Event Loop)實現了異步IO,它們是:twisted: 是事件驅動的網絡庫
gevent: greenlet + libevent(后來是libev或libuv)。通過協程(greenlet)和事件循環庫(libev,libuv)實現的gevent使用很廣泛。
tornado: 支持異步IO的web框架。自己實現了IOLOOP。
Python 3 官方的異步IO
Python 3.4 加入了asyncio 庫,使得Python有了支持異步IO的官方庫。這個庫,底層是事件循環(EventLoop),上層是協程和任務。asyncio自從3.4 版本加入到最新的 3.7版一直在改進中。Python 3.4 剛開始的asyncio的協程還是基于生成器的,通過 yield from 語法實現,可以通過裝飾器?@asyncio.coroutine(已過時)裝飾一個函數來定義一個協程。比如:
Python 3.5 引入了兩個新的關鍵字 await 和 async 用來替換 @asyncio.coroutine 和 yield from ,從語言本身來支持異步IO。從而使得異步編程更加簡潔,并和普通的生成器區別開來。注意:對基于生成器的協程的支持已棄用,并計劃在 Python 3.10 中移除。所以,寫異步IO程序時只需使用 async 和 await 即可。Python 3.7 又進行了優化,把API分組為高層級API和低層級API。我們先看看下面的代碼,發現與上面的有什么不同?除了用 async 替換 @asyncio.coroutine 和用 await 替換 yield from 外,最大的變化就是關于eventloop的代碼不見了,只有一個 async.run()。這就是 3.7 的改進,把eventloop相關的API歸入到低層級API,新引進run()作為高層級API讓寫應用程序的開發者調用,而不用再關心eventloop。除非你要寫異步庫(比如MySQL異步庫)才會和eventloop打交道。
理解asyncio
理解asyncio并不能,關鍵是要動起手來,接下來我們以下面代碼為例動手實踐一番,通過實踐來理解它。
這段代碼很簡單,我們定義了兩個協程函數(在def前面加async),其中 hi()?我們把它叫做功能函數,通過一個 aysncio.sleep() 來模擬一個耗時的異步IO操作(比如下載網頁), main() 叫做入口函數。其實就是在main() 里面調用 hi() 函數,通過不斷改變 main() 的行為來理解異步IO(協程函數的調用)的運行過程。
1. 協程函數如何運行?
首先,我們要明確一個道理,hi() 是一個協程函數,直接調用它返回的是一個協程對象,并沒有真正運行它。把main函數改成如下,我們來仔細看看協程函數 hi() 的運行。
下面是運行結果:
代碼第19行,我們像運行普通函數一樣運行 hi() ,得到的a只是一個協程對象,見結果第二行:
a is: 這個協程對象 a 雖然生成了,但是還沒有運行,它需要一個時機。也就是asyncio的事件循環正在運行main,還沒有空去運行它。
代碼第21行,通過 await 告訴 event_loop(事件循環)?,main協程停在這里,你去運行其它協程吧。這時候 event_loop 去執行a協程,也就是去執行 hi() 函數里面的代碼。等 hi() 運行完,event_loop 再回到main協程繼續從21行開始執行,把 hi() 的返回值賦值給b,這時候 b 的值是1。
event_loop 在整個異步IO過程中扮演一個管家的角色,在不同的協程之間切換運行代碼,切換是通過事件來進行的,通過 await 離開當前協程,await 的協程完成后又回到之前的協程對應的地方繼續執行。
2. 協程函數如何并發?
異步IO的好處就是并發,但如何實現呢?我們先來看一個不是并發的例子:
這次,我們把main修改成一個for循環執行4次 hi()?,看看它運行的結果:
整個過程從21:48:30 到 21:48:40 結束,用了10秒。而hi()的執行時間分別是1秒,2秒,3秒,4秒總共10秒。也就是4個hi() 雖然是異步的但是順序執行的,沒有并發。
接下來,就到了并發的實現了,通過 asyncio.creat_task()?即可:
通過 create_task() 我們在for循環里面生成了4個task(也是協程對象),但是這4個協程任務并沒有被執行,它們需要等待一個時機:當前協程(main)遇到 await。
第二個for循環開始逐一 await 協程,此時 event_loop 就可以空出手來去執行那4個協程,過程大致如下:先執行hi(1, 1) ,打印“enter hi(), 1 @21:58:35”,遇到await asyncio.sleep(1),當前協程掛起;
接著執行 hi(2, 2),執行打印命令,遇到await asyncio.sleep(2)?,當前協程掛起;
接著執行 hi(3, 3),執行打印命令,遇到await asyncio.sleep(3)?,當前協程掛起;
接著執行 hi(4, 4),執行打印命令,遇到await asyncio.sleep(4)?,當前協程掛起;
以上4步只是協程的切換和打印語句,執行非常快,我們可以任務它們是同時執行起來的。
1秒后,hi(1,1)的sleep結束它會發出事件告訴 event_loop 我await結束了,過來執行我,event_loop 此時空閑就來執行它,繼續執行sleep后面的打印語句;
2秒后,hi(2,2)的sleep結束它會發出事件告訴 event_loop 我await結束了,過來執行我,event_loop 此時空閑就來執行它,繼續執行sleep后面的打印語句;
3秒后,hi(3,3)的sleep結束它會發出事件告訴 event_loop 我await結束了,過來執行我,event_loop 此時空閑就來執行它,繼續執行sleep后面的打印語句;
4秒后,hi(4,4)的sleep結束它會發出事件告訴 event_loop 我await結束了,過來執行我,event_loop 此時空閑就來執行它,繼續執行sleep后面的打印語句;
4秒后,生成的4個協程任務就都執行完畢。總耗時4秒,也就是我們的4個任務并發完成了。
所以,上面的代碼運行的結果如下:
根據上面講述的執行流程,可以看到結果對應起來了。4個任務都是在35秒時開始執行,以后每個1秒完成一個。main函數從35執行到39介紹,共耗時4秒。
3. 錯誤的運行
上面的并發很完美,但有時候你可能會犯錯。比如下面的main(), 你可能只是并發 hi() 函數,但不需要它的返回結果,于是有了下面的 main():
先猜猜會有什么樣的結果!!
你猜對了嗎?下面是運行結果:main()的for循環只是生成了4個task協程,然后就退出了。event_loop 收到main退出的事件就空出來去執行了那4個協程,進去了但都碰到了sleep。然后event_loop就空閑了。這時候run() 就收到了main() 執行完畢的事件,run() 就執行完了,最后執行print,整個程序就退出了。從main退出到整個程序退出就是一瞬間的事情,那4個協程還在傻傻的睡著,不,是在睡夢中死去了。
在main()中加一個sleep會出現什么結果:
在main()退出前,我們要先sleep 2秒,再來猜猜它的運行結果是什么?
如果你對上面沒有sleep的過程搞清楚了,不難猜到正確的結果:
注意:main() 的退出和 hi(2, 2) 的退出順序。簡單講,main() 先sleep 2秒,hi(2, 2) 后sleep兩秒,所以main先退出。
理解了sleep(2) 的執行過程,那么你就可以知道 sleep(4) 和 sleep(5) 的結果了。如果沒有自信的話,就自己改一下時間,運行看看結果。
4. 如何判斷是否要把函數定義為協程函數?
定義一個協程函數很簡單,在def前面加async即可。那么如何判斷一個函數該不該定義為協程函數呢?
記住這一個原則:如果該函數是要進行IO操作(讀寫網絡、讀寫文件、讀寫數據庫等),就把它定義為協程函數,否則就是普通函數。
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的pythonasyncio在哪个版本好_理解Python asyncio的简洁方式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java客户端传递参数_java –
- 下一篇: python如何导入matlab数据,p