Python併發程式設計之協程/非同步IO

發表於2017-01-06

協程與非同步IO

引言

隨著node.js的盛行,相信大家今年多多少少都聽到了非同步程式設計這個概念。Python社群雖然對於非同步程式設計的支援相比其他語言稍顯遲緩,但是也在Python3.4中加入了asyncio,在Python3.5上又提供了async/await語法層面的支援,剛正式釋出的Python3.6中asynico也已經由臨時版改為了穩定版。下面我們就基於Python3.4+來了解一下非同步程式設計的概念以及asyncio的用法。

什麼是協程

通常在Python中我們進行併發程式設計一般都是使用多執行緒或者多程式來實現的,對於計算型任務由於GIL的存在我們通常使用多程式來實現,而對與IO型任務我們可以通過執行緒排程來讓執行緒在執行IO任務時讓出GIL,從而實現表面上的併發。

其實對於IO型任務我們還有一種選擇就是協程,協程是執行在單執行緒當中的“併發”,協程相比多執行緒一大優勢就是省去了多執行緒之間的切換開銷,獲得了更大的執行效率。Python中的asyncio也是基於協程來進行實現的。在進入asyncio之前我們先來了解一下Python中怎麼通過生成器進行協程來實現併發。

example1

我們先來看一個簡單的例子來了解一下什麼是協程(coroutine),對生成器不瞭解的朋友建議先看一下Stackoverflow上面的這篇高票回答

example2

下面這個程式我們要實現的功能就是模擬多個學生同時向一個老師提交作業,按照傳統的話我們或許要採用多執行緒/多程式,但是這裡我們可以採用生成器來實現協程用來模擬併發。

如果下面這個程式讀起來有點困難,可以直接跳到後面部分,並不影響閱讀,等你理解協程的本質,回過頭來看就很簡單了。

下面我們來呼叫一下這個程式。

這是輸出結果,我們僅僅只用了一個簡單的生成器就實現了併發(concurrence),注意不是並行(parallel),因為我們的程式僅僅是執行在一個單執行緒當中。

##使用asyncio模組實現協程

從Python3.4開始asyncio模組加入到了標準庫,通過asyncio我們可以輕鬆實現協程來完成非同步IO操作。

解釋一下下面這段程式碼,我們創造了一個協程display_date(num, loop),然後它使用關鍵字yield from來等待協程asyncio.sleep(2)的返回結果。而在這等待的2s之間它會讓出CPU的執行權,直到asyncio.sleep(2)返回結果。

下面是執行結果,注意到併發的效果沒有,程式從開始到結束只用大約10s,而在這裡我們並沒有使用任何的多執行緒/多程式程式碼。在實際專案中你可以將asyncio.sleep(secends)替換成相應的IO任務,比如資料庫/磁碟檔案讀寫等操作。

在Python3.5中為我們提供更直接的對協程的支援,引入了async/await關鍵字,上面的程式碼我們可以這樣改寫,使用async代替了@asyncio.coroutine,使用了await代替了yield from,這樣我們的程式碼變得更加簡潔可讀。

asyncio模組詳解

開啟事件迴圈有兩種方法,一種方法就是通過呼叫run_until_complete,另外一種就是呼叫run_forever。run_until_complete內建add_done_callback,使用run_forever的好處是可以通過自己自定義add_done_callback,具體差異請看下面兩個例子。

run_until_complete()

run_forever()

run_forever相比run_until_complete的優勢是新增了一個add_done_callback,可以讓我們在task(future)完成的時候呼叫相應的方法進行後續處理。

這裡還要注意一點,即使你呼叫了協程方法,但是如果事件迴圈沒有開啟,協程也不會執行,參考官方文件的描述,我剛被坑過。

Calling a coroutine does not start its code running – the coroutine object returned by the call doesn’t do anything until you schedule its execution. There are two basic ways to start it running: call await coroutine or yield from coroutine from another coroutine (assuming the other coroutine is already running!), or schedule its execution using the ensure_future() function or the AbstractEventLoop.create_task() method. Coroutines (and tasks) can only run when the event loop is running.

Call

call_soon()

下面是執行結果,我們可以通過call_soon提前註冊我們的task,並且也可以根據返回的Handle進行cancel。

call_later()

改動一下上面的例子我們來看一下call_later的用法,注意這裡並沒有像上面那樣使用while迴圈進行操作,我們可以通過call_later來設定每隔1秒去呼叫display_date()方法。

Chain coroutines

下面是輸出結果

在爬蟲中使用asyncio來實現非同步IO

下面我們來通過一個簡單的例子來看一下怎麼在Python爬蟲專案中使用asyncio。by the way: 根據我有限的實驗結果,如果要充分發揮asynio的威力,應該使用aiohttp而不是requests。而且也要合理使用concurrent.futures模組提供的執行緒池/程式池,這一點我會在下一篇博文描述。

p.s: 如果你能自己體會到為什麼盲目地使用執行緒池/程式池並不能提高基於asynico模組的程式的效率,我想你對協程的理解也差不多了。

References

DOCUMENTATION OF ASYNCIO
COROUTINES AND ASYNC/AWAIT
STACKOVERFLOW
PyMOTW-3

相關文章