python的asyncio庫以協程爲基礎,event_loop做爲協程的驅動和調度模型。該模型是一個單線程的異步模型,相似於node.js。下圖我所理解的該模型node
事件循環經過select()來監聽是否存在就緒的事件,若是存在就把事件對應的callback添加到一個task list中。而後從task list頭部中取出一個task執行。在單線程中不斷的註冊事件,執行事件,從而實現了咱們的event_loop模型。python
event_loop中執行的task並非函數mysql
若是咱們把上圖當成一個web服務器,左邊的一個task當成一次http請求須要執行的完整任務。若是咱們每一次run_task()都執行完一個完整的任務,再去run下一個task。 那這跟普通的串行服務器並無區別。在併發環境下形成的用戶體驗很是差。web
具體怎麼差你能夠腦補一下,畢竟咱們如今是使用單線程方式實現的web服務器sql
因此task若是對應一個完整的http請求那麼其不多是一個函數,由於函數須要從頭執行到尾佔用着整個線程。那你以爲task是什麼呢?編程
若是你不知道答案的話能夠看一看個人另外一篇文章 簡述python的yield和yield from服務器
沒錯,task是一個generator,或者能夠叫作可中斷的函數。task的代碼依舊是從上寫到下來處理一個http請求。也就是咱們所說的同步的代碼組織。網絡
可是有所不一樣的是,在task中,咱們遇到i/o操做時,咱們就把i/o操做交給selector(稍後咱們解析一下selector,而且把該i/o操做準備完畢後須要執行的回調也告訴selector。而後咱們使用yield保存並中斷該函數。併發
此時線程的控制權回到event_loop手中。event_loop首先看一下selector中是否存在就緒的數據,存在的話就把對應的回調放到task list的尾部(如圖),而後從頭部繼續run_task()。異步
你可能想問上面中斷的task何時才能繼續執行呢?我前一句說過了,event_loop每一次循環都會檢測selector中是否存在就緒的i/o操做,若是存在就緒的i/o操做,咱們對應就把callback放到task的尾部,當event_loop執行到這個task時。咱們就能回到咱們剛剛中斷的函數繼續執行啦,並且此時咱們須要的i/o操做獲得的數據也已經準備好了。
這種操做若是你站在函數的角度會有種神奇的感受,在函數眼裏,本身須要get遙遠服務器的一些數據,因而調動get(),而後瞬間就獲得了遙遠服務器的數據。沒錯在函數的眼裏就是瞬間獲得,這感受就彷彿是穿越到了將來同樣。
你可能又想問,爲何把callback放到task,而後run一下就回到原有的函數執行位置了?
這我也不知道,我並無深追asyncio的代碼,這對於我來講有些複雜。但若是是個人話,我只要在callback中設置一個變量gen指向咱們的generator就好了,而後只要在callback中gen.send(res_data)
,咱們就能回到中斷處繼續執行了。若是你有興趣的話能夠本身使用debug來追一下代碼。
不過我更推薦你閱讀一下這篇博文 深刻理解 Python 異步編程(上)
好比咱們在task中須要執行一個1+2+3+到2000萬這樣一個操做,這個操做耗時有些長,並且不屬於i/o操做,無法交給selector去調度,此時咱們須要本身yield,讓其餘的task能有機會來使用咱們惟一的線程。這樣就又有一個新的問題。yield後,咱們何時再次來執行這個被中斷的函數呢?
問題代碼示例
import asyncio
def print_sum():
sum = 0
count = 0
for a in range(20000000):
sum += a
count += 1
if count > 1000000:
count = 0
yield
print('1+到2000萬的和是{}'.format(sum))
@asyncio.coroutine
def init():
yield from print_sum()
loop = asyncio.get_event_loop()
loop.run_until_complete(init())
loop.run_forever()複製代碼
我想咱們能夠這樣,把這個中斷的task直接加入到task list的尾部,而後繼續event_loop,這樣讓其餘task有機會執行,而且處理起來更加的簡單。 asyncio庫也確實是這樣作的。
可是asyncio還提供了更好的作法,咱們能夠再啓動一個線程來執行這種cpu密集型運算
再來看看另一個問題。若是在一個凌晨三點半,你task list此時是空的,那麼你的event_loop怎麼運做?繼續不停的loop等待新的http請求進來? no,咱們不容許如此浪費cpu的資源。asyncio庫也不容許。
首先看兩行event_loop中的代碼片斷,也就是上圖中右上角部分的select(timeout)部分
event_list = self._selector.select(timeout)
self._process_events(event_list)複製代碼
補充一點,做爲一臺web服務器,咱們老是須要socket()、bind()、listen()、來建立一個監聽描述符sockfd,用來監聽到來的http請求,與http請求完成三路握手。而後經過accept()操做來獲得一個已鏈接描述符connectfd。
這裏的兩個文件描述符,此時都存在於咱們的系統中,其中sockfd繼續用來執行監聽http請求操做。已經鏈接了的客戶端咱們則經過connectfd來與其通訊。通常都是一個sockfd對多個connectfd。
更多的細節推薦閱讀 ——《unix網絡編程卷一》中的關於socket編程的幾章
asyncio對於網絡i/o使用了 selector模塊,selector模塊的底層則是由 epoll()來實現。也就是一個同步的i/o複用系統調用(你定會驚訝於asyncio的居然使用了同步i/o來實現?咱們在下一節來解讀一下epoll函數)
這裏你能夠去讀一下python手冊中的selector模塊,看看這個模塊的做用
epoll()函數有個timeout參數,用來控制該函數是否阻塞,阻塞多久。映射到高層就是咱們上面的selector.select(timeout)
中的timeout。原來咱們的event_loop中的存在一個timeout。這樣凌晨三點半咱們如何處理event_loop我想你已經內心有數了吧。
asyncio的實現和你想的差很少。若是task list is not None那麼咱們的timeout=0也就是非阻塞的。解釋一下就是,咱們調用selector.select(timeout = 0 ),該函數會立刻返回結果,咱們對結果作一個上面講過的處理,也就是self._process_events(event_list)
。而後咱們繼續run task。
若是咱們的task list is None, 那麼咱們則把timeout=None。也就是設置成阻塞操做。此時咱們的代碼或者說線程會阻塞在selector.select(timeout = 0)處,換句話說就是等待該函數的返回。固然這樣作的前提是,你往selector中註冊了須要等待的socket描述符。
還有一些其餘的問題,好比異步mysql是如何在asyncio的基礎上實現的,這可能須要去閱讀aiomysql庫了。
你也許發現,咱們一旦使用了event_loop實現單線程異步服務器,咱們寫的全部代碼就都不是咱們來控制執行了,代碼的執行權所有交給了event_loop,event_loop在適當的時間run task。讀過廖雪峯python教程的小夥伴必定看過這句話
這就是異步編程的一個原則:一旦決定使用異步,則系統每一層都必須是異步,「開弓沒有回頭箭」。
這就是異步編程。
你也許對asyncio的做用,或者使用,或者代碼實現有着不少的疑問,我也是如此。可是很抱歉,我並不怎麼熟悉python,也沒有使用asyncio作過項目,只是出於好奇因此我對python的異步i/o進行了一個瞭解。
我是一個紙上談兵的門外漢,到最後我也沒能看清asyncio庫的具體實現。我接下來的計劃中並不打算對asyncio庫進行更多的研究,可是我又不甘心這兩天對asyncio庫的研究付諸東流。因此我留下這篇博文,算是對本身的一個交待!但願下次可以有機會,可以更加了解python和asyncio的前提下,再寫一篇深刻解析python—asyncio的博文。