asyncio是Python3.4引入的一個標準庫,直接內(nèi)置了對異步IO的支持。asyncio模塊提供了使用協(xié)程構(gòu)建并發(fā)應用的工具。它使用一種單線程單進程的的方式實現(xiàn)并發(fā),應用的各個部分彼此合作, 可以顯示的切換任務,一般會在程序阻塞I/O操作的時候發(fā)生上下文切換如等待讀寫文件,或者請求網(wǎng)絡。同時asyncio也支持調(diào)度代碼在將來的某個特定事件運行,從而支持一個協(xié)程等待另一個協(xié)程完成,以處理系統(tǒng)信號和識別其他一些事件。 異步并發(fā)的概念對于其他的并發(fā)模型大多數(shù)采取的都是線性的方式編寫。并且依賴于語言運行時系統(tǒng)或操作系統(tǒng)的底層線程或進程來適當?shù)馗淖兩舷挛?,而基于asyncio的應用要求應用代碼顯示的處理上下文切換。 asyncio提供的框架以事件循環(huán)(event loop)為中心,程序開啟一個無限的循環(huán),程序會把一些函數(shù)注冊到事件循環(huán)上。當滿足事件發(fā)生的時候,調(diào)用相應的協(xié)程函數(shù)。 事件循環(huán)事件循環(huán)是一種處理多并發(fā)量的有效方式,在維基百科中它被描述為「一種等待程序分配事件或消息的編程架構(gòu)」,我們可以定義事件循環(huán)來簡化使用輪詢方法來監(jiān)控事件,通俗的說法就是「當A發(fā)生時,執(zhí)行B」。事件循環(huán)利用poller對象,使得程序員不用控制任務的添加、刪除和事件的控制。事件循環(huán)使用回調(diào)方法來知道事件的發(fā)生。它是asyncio提供的「中央處理設備」,支持如下操作: 注冊、執(zhí)行和取消延遲調(diào)用(超時) 創(chuàng)建可用于多種類型的通信的服務端和客戶端的Transports 啟動進程以及相關(guān)的和外部通信程序的Transports 將耗時函數(shù)調(diào)用委托給一個線程池 單線程(進程)的架構(gòu)也避免的多線程(進程)修改可變狀態(tài)的鎖的問題。 與事件循環(huán)交互的應用要顯示地注冊將運行的代碼,讓事件循環(huán)在資源可用時向應用代碼發(fā)出必要的調(diào)用。如:一個套接字再沒有更多的數(shù)據(jù)可以讀取,那么服務器會把控制全交給事件循環(huán)。 Futurefuture是一個數(shù)據(jù)結(jié)構(gòu),表示還未完成的工作結(jié)果。事件循環(huán)可以監(jiān)視Future對象是否完成。從而允許應用的一部分等待另一部分完成一些工作。 Tasktask是Future的一個子類,它知道如何包裝和管理一個協(xié)程的執(zhí)行。任務所需的資源可用時,事件循環(huán)會調(diào)度任務允許,并生成一個結(jié)果,從而可以由其他協(xié)程消費。 異步方法使用asyncio也就意味著你需要一直寫異步方法。 一個標準方法是這樣的: 而一個異步方法: 從外觀上看異步方法和標準方法沒什么區(qū)別只是前面多了個async。 “Async” 是“asynchronous”的簡寫,為了區(qū)別于異步函數(shù),我們稱標準函數(shù)為同步函數(shù), 從用戶角度異步函數(shù)和同步函數(shù)有以下區(qū)別: 要調(diào)用異步函數(shù),必須使用await關(guān)鍵字。 因此,不要寫regular_double(3),而是寫await async_double(3). 不能在同步函數(shù)里使用await,否則會出錯。 句法錯誤: 但是在異步函數(shù)中,await是被允許的: 協(xié)程 啟動一個協(xié)程 一般異步方法被稱之為協(xié)程(Coroutine)。asyncio事件循環(huán)可以通過多種不同的方法啟動一個協(xié)程。一般對于入口函數(shù),最簡答的方法就是使用run_until_complete,并將協(xié)程直接傳入這個方法。 輸出 這就是最簡單的一個協(xié)程的例子,下面讓我們了解一下上面的代碼. 第一步首先得到一個事件循環(huán)的應用也就是定義的對象loop??梢允褂媚J的事件循環(huán),也可以實例化一個特定的循環(huán)類(比如uvloop),這里使用了默認循環(huán)run_until_complete(coro)方法用這個協(xié)程啟動循環(huán),協(xié)程返回時這個方法將停止循環(huán)。 run_until_complete的參數(shù)是一個futrue對象。當傳入一個協(xié)程,其內(nèi)部會自動封裝成task,其中task是Future的子類。關(guān)于task和future后面會提到。 從協(xié)程中返回值將上面的代碼,改寫成下面代碼 run_until_complete可以獲取協(xié)程的返回值,如果沒有給定返回值,則像函數(shù)一樣,默認返回None。 協(xié)程調(diào)用協(xié)程一個協(xié)程可以啟動另一個協(xié)程,從而可以任務根據(jù)工作內(nèi)容,封裝到不同的協(xié)程中。我們可以在協(xié)程中使用await關(guān)鍵字,鏈式的調(diào)度協(xié)程,來形成一個協(xié)程任務流。向下面的例子一樣。 輸出 協(xié)程中調(diào)用普通函數(shù) 在協(xié)程中可以通過一些方法去調(diào)用普通的函數(shù)??梢允褂玫年P(guān)鍵字有call_soon,call_later,call_at。 call_soon 可以通過字面意思理解調(diào)用立即返回。 loop.call_soon(callback, *args, context=None) 在下一個迭代的時間循環(huán)中立刻調(diào)用回調(diào)函數(shù),大部分的回調(diào)函數(shù)支持位置參數(shù),而不支持”關(guān)鍵字參數(shù)”,如果是想要使用關(guān)鍵字參數(shù),則推薦使用functools.aprtial對方法進一步包裝.可選關(guān)鍵字context允許指定要運行的回調(diào)的自定義contextvars.Context。當沒有提供上下文時使用當前上下文。在Python 3.7中, asyncio 協(xié)程加入了對上下文的支持。使用上下文就可以在一些場景下隱式地傳遞變量,比如數(shù)據(jù)庫連接session等,而不需要在所有方法調(diào)用顯示地傳遞這些變量。 下面來看一下具體的使用例子。 輸出結(jié)果 通過輸出結(jié)果我們可以發(fā)現(xiàn)我們在協(xié)程中成功調(diào)用了一個普通函數(shù),順序的打印了1和2。 有時候我們不想立即調(diào)用一個函數(shù),此時我們就可以call_later延時去調(diào)用一個函數(shù)了。 call_later loop.call_later(delay, callback, *args, context=None) 首先簡單的說一下它的含義,就是事件循環(huán)在delay多長時間之后才執(zhí)行callback函數(shù). 配合上面的call_soon讓我們看一個小例子 輸出 通過上面的輸出可以得到如下結(jié)果: 1.call_soon會在call_later之前執(zhí)行,和它的位置在哪無關(guān) 2.call_later的第一個參數(shù)越小,越先執(zhí)行。 call_atloop.call_at(when, callback, *args, context=None) call_at第一個參數(shù)的含義代表的是一個單調(diào)時間,它和我們平時說的系統(tǒng)時間有點差異, 這里的時間指的是事件循環(huán)內(nèi)部時間,可以通過loop.time獲取,然后可以在此基礎上進行操作。后面的參數(shù)和前面的兩個方法一樣。實際上call_later內(nèi)部就是調(diào)用的call_at。 輸出 因為call_later內(nèi)部實現(xiàn)就是通過call_at所以這里就不多說了。 Future獲取Futrue里的結(jié)果 future表示還沒有完成的工作結(jié)果。事件循環(huán)可以通過監(jiān)視一個future對象的狀態(tài)來指示它已經(jīng)完成。future對象有幾個狀態(tài): Pending Running Done Cancelled 創(chuàng)建future的時候,task為pending,事件循環(huán)調(diào)用執(zhí)行的時候當然就是running,調(diào)用完畢自然就是done,如果需要停止事件循環(huán),就需要先把task取消,狀態(tài)為cancel。 輸出 可以通過輸出結(jié)果發(fā)現(xiàn),調(diào)用set_result之后future對象的狀態(tài)由pending變?yōu)閒inished,F(xiàn)uture的實例all_done會保留提供給方法的結(jié)果,可以在后續(xù)使用。 Future對象使用awaitfuture和協(xié)程一樣可以使用await關(guān)鍵字獲取其結(jié)果。 ![]() Future回調(diào) Future 在完成的時候可以執(zhí)行一些回調(diào)函數(shù),回調(diào)函數(shù)按注冊時的順序進行調(diào)用: ![]() 通過add_done_callback方法給funtrue任務添加回調(diào)函數(shù),當funture執(zhí)行完成的時候,就會調(diào)用回調(diào)函數(shù)。并通過參數(shù)future獲取協(xié)程執(zhí)行的結(jié)果。 到此為止,我們就學會了如何在協(xié)程中調(diào)用一個普通函數(shù)并獲取其結(jié)果。 并發(fā)的執(zhí)行任務任務(Task)是與事件循環(huán)交互的主要途徑之一。任務可以包裝協(xié)程,可以跟蹤協(xié)程何時完成。任務是Future的子類,所以使用方法和future一樣。協(xié)程可以等待任務,每個任務都有一個結(jié)果,在它完成之后可以獲取這個結(jié)果。 因為協(xié)程是沒有狀態(tài)的,我們通過使用create_task方法可以將協(xié)程包裝成有狀態(tài)的任務。還可以在任務運行的過程中取消任務。 ![]() 輸出 如果把上面的task.cancel注釋了我們可以得到正常情況下的結(jié)果,如下。 另外出了使用loop.create_task將協(xié)程包裝為任務外還可以使用asyncio.ensure_future(coroutine)建一個task。在python3.7中可以使用asyncio.create_task創(chuàng)建任務。 組合協(xié)程一系列的協(xié)程可以通過await鏈式的調(diào)用,但是有的時候我們需要在一個協(xié)程里等待多個協(xié)程,比如我們在一個協(xié)程里等待1000個異步網(wǎng)絡請求,對于訪問次序有沒有要求的時候,就可以使用另外的關(guān)鍵字wait或gather來解決了。wait可以暫停一個協(xié)程,直到后臺操作完成。 等待多個協(xié)程 Task的使用 ![]() 輸出 ![]() 可以發(fā)現(xiàn)我們的結(jié)果并沒有按照數(shù)字的順序顯示,在內(nèi)部wait使用一個set保存它創(chuàng)建的Task實例。因為set是無序的所以這也就是我們的任務不是順序執(zhí)行的原因。wait的返回值是一個元組,包括兩個集合,分別表示已完成和未完成的任務。wait第二個參數(shù)為一個超時值 達到這個超時時間后,未完成的任務狀態(tài)變?yōu)閜ending,當程序退出時還有任務沒有完成此時就會看到如下的錯誤提示。 ![]() 此時我們可以通過迭代調(diào)用cancel方法取消任務。也就是這段代碼 gather的使用 gather的作用和wait類似不同的是。 1.gather任務無法取消。 2.返回值是一個結(jié)果列表 3.可以按照傳入?yún)?shù)的順序,順序輸出。 我們將上面的代碼改為gather的方式 ![]() 輸出 gather通常被用來階段性的一個操作,做完第一步才能做第二步,比如下面這樣 ![]() 輸出 ![]() 可以通過上面結(jié)果得到如下結(jié)論: 1.step1和step2是并行運行的。 2.gather會等待最耗時的那個完成之后才返回結(jié)果,耗時總時間取決于其中任務最長時間的那個。 任務完成時進行處理as_complete是一個生成器,會管理指定的一個任務列表,并生成他們的結(jié)果。每個協(xié)程結(jié)束運行時一次生成一個結(jié)果。與wait一樣,as_complete不能保證順序,不過執(zhí)行其他動作之前沒有必要等待所以后臺操作完成。 ![]() 輸出 ![]() 可以發(fā)現(xiàn)結(jié)果逐個輸出。 到此為止第一部分就結(jié)束了,對于asyncio入門級學習來說這些內(nèi)容就夠了。如果想繼續(xù)跟進asyncio的內(nèi)容,敬請期待后面的內(nèi)容。 |
|