Python 支援重啟的非同步 IO

pythontab發表於2013-03-25

摘要


這是一份從Python3.3開始的Python3非同步I/O提議。研究從PEP 3153缺失的具體提議。 這提議包括了一個可插入式的事件迴圈API,傳輸和與Twisted相似的協議抽象,以及來自(PEP 380) 基於yield的更高階的排程器。一份作品裡的參考實現,它的程式碼命名為Tulip(Tulip的repo的連結放在文章最後的參考文獻分段裡)。

介紹


事件迴圈常用於互操作性較高的地方。對於像Twisted、Tornado或者ZeroMQ這類(基於Python3.3的)框架,它應該是容易去根據框架的需求透過輕量級封裝或者代理去適配預設的事件迴圈實現,或者是用它們自己的事件迴圈實現去替代預設的實現。(一些像Twisted的框架,擁有多種的事件迴圈實現。由於這些實現都具有統一的介面,所以這應該不會成為問題。)

事件迴圈甚至有可能存在兩個不同的第三方框架互動,透過共享預設事件迴圈實現(各自使用自己的介面卡),或者是透過共享其中一個框架的事件迴圈實現。在後者,兩種不同級別的適配可能會存在(從框架A的事件迴圈到標準事件迴圈介面,然後從標準的再到框架B的事件迴圈)。被使用的事件迴圈實現應該在主程式的控制之下(儘管提供了事件迴圈可以選擇的預設策略)。

因此,兩個單獨的API被定義:

獲取和設定當前事件的迴圈物件;

一個確認事件迴圈物件的介面和它的最低保證

一個事件迴圈實現可能提供額外的方法和保證。

事件迴圈介面不取決於產量,相反,它使用一個回撥,額外介面(傳輸協議和協議)以及期貨(?)的組合。後者類似於在PEP3148中定義的介面,但有不同的實現而且不繫結到執行緒。特別是,它們沒有wait()方法,使用者將使用回撥。

對於那些不喜歡用回撥函式的人(包括我),(Python)提供了一個寫非同步I/O程式碼的排程器作為協同程式,它使用了PEP 380的yield from表示式。這個排程器並不是可插入的;可插入性存在於事件迴圈級別,同時排程器也應該工作在任何符合標準的事件迴圈實現上。

對於那些在使用協同程式和其他非同步框架的程式碼的互操作性,排程器有一個行為上類似Future的Task類。一個在事件迴圈級別進行互動操作的框架能夠透過加入一個回撥函式到Future中,同時等待一個Future來完成。同樣的,排程器提供一個操作來掛起協同程式,直到回撥函式被呼叫。

透過事件迴圈介面來為執行緒間互動操作提供限制;(Python裡)有一個API能夠提交一個函式到一個能夠返回相容事件迴圈的Future的執行器(看 PEP 3148)。

沒有目的的


像Stackless Python或 greenlets/gevent 的系統互操作性不是本教程的目的。

規範


依賴


Python3.3是必需的。不需超過Python3.3範圍的新語言或標準庫。不需要第三方模組或包。

模組名稱空間


這裡的規範會放在一個新的頂層包。不同的元件會放在這個包的不同子模組裡。包會從各自的子模組中匯入常用的API,同時使他們能作為包的可用屬性(類似電子郵件包的做法)。

頂層包的名字目前還沒指定。參考實現使用“tulip”來命名,但是這個名字可能會在這個實現加入到標準庫的時候改變為其他更為煩人的名字(有希望在Python3.4中)。

在煩人的名字選好之前,這篇教程會使用“tulip”作為頂層包的名字。假定沒有給定模組名的類和函式都透過頂層包來訪問。

事件迴圈策略:獲取和設定事件迴圈


要獲取當前的事件迴圈,可以使用get_event_loop()。這函式返回一個在下面有定義的EventLoop類的例項或者一個等價的物件。get_event_loop()可能根據當前執行緒返回不同的物件,或者根據其他上下文的概念返回不同物件。

要設定當前事件迴圈,可以使用set_event_loop(event_loop),這裡的event_loop是EventLoop類的例項或者等價的例項物件。這裡使用與get_event_loop()相同的上下文的概念。

還有第三個策略函式:new_event_loop(),有利於單元測試和其他特別的情況,它會建立和返回一個新的基於該策略的預設規則的EventLoop例項。要使它成為當前的事件迴圈,你需要呼叫set_event_loop()。

要改變上述三個函式的工作方式(包括他們的上下文的概念),可以透過呼叫set_event_loop_policy(policy),其中引數policy是一個事件迴圈策略物件。這個策略物件可以是任何包含了類似上面描述的函式表現(get_event_loop(),set_event_loop(event_loop)和new_event_loop())的物件。預設的事件迴圈策略是DefaultEventLoopPolicy類的一個例項。當前事件迴圈策略物件能夠透過呼叫get_event_loop_policy()來取回。

一個事件迴圈策略沒強制要求只能有一個事件迴圈存在。預設的事件迴圈策略也沒強制要求這樣做,但是它強制要求每個執行緒只能有一個事件迴圈。

事件迴圈介面


關於時間:在 Python 中,所有的超時(timeout),間隔(interval)和延時(delay)都是以秒來計算的,可以是整型也可以是浮點型。時鐘的精準度依賴於具體的實現;預設使用 time.monotonic()。

關於回撥(callbacks)和處理函式(handlers):如果一個函式接受一個回撥函式和任意個數的變數作為引數,那麼你也可以用一個處理函式物件(Handler)來替換回撥函式。這樣的話就不需要再傳遞那些引數。這個處理函式物件應該是一個立即返回的函式(fromcall_soon()),而不是延遲返回的(fromcall_later())。如果處理函式已經取消,那麼這個呼叫將不起作用。

一個符合標準的事件迴圈物件擁有以下的方法:

run()。 執行事件迴圈,知道沒啥好做了。具體的意思是:

除了取消呼叫外,沒有更多透過call_later(),call_repeatedly(),call_soon(), orcall_soon_threadsafe()這些方法排程的呼叫。

沒有更多的註冊了的檔案描述符。 當它關閉的時候會由註冊方來登出檔案描述符。

備註:直到遇到終止條件或者呼叫stop(),run()會一直阻塞。

備註: 如果你使用call_repeatedly()來執行一個呼叫,run()不會在你呼叫stop()前退出。

需要詳細說明: 有多少類似的真正需要我們做的?

run_forever()。直到呼叫stop()前一直執行事件迴圈。

run_until_complete(future, timeout=None)。在Future完成前一直執行事件迴圈。如果給出了timeout的值,它會等待timeout的時間。 如果Future完成了,它的結果會返回 或者它的異常丟擲;如果在超時前完成Future, 或者stop()被呼叫,會丟擲TimeoutError (但Future不會被取消). 在事件迴圈已經在執行的時候,這個方法不能呼叫。

備註: 這個API更多用來做測試或者類似的工作。 它不應該用作從yield from 表示式的future替代品或其他等待一個Future的方法。 (例如 註冊一個完成的回撥)。

run_once(timeout=None)。執行事件迴圈一段事件。 如果給出了timeout的值, I/O輪詢會阻塞一段時間; 否則, I/O輪詢不會受時間約束。

備註:準確來說,這裡做了多少工作是根據具體實現的。 一個約束是:如果一個使用call_soon()來直接排程自己,會導致死順壞,run_once()仍然會返回。

stop()。儘可能快地停止事件迴圈。隨後可以使用run()重啟迴圈(或者的一個變體)。

備註: 有多塊來停止是根據它具體實現。所有在stop()前已經在執行的直接回撥函式必定仍在執行,但是在stop()呼叫後的排程的回撥函式(或者延遲執行的)不會執行。

close()。關閉事件迴圈,釋放它所保持的所有資源,例如被epoll()或kqueue()使用的檔案描述符。這個方法不應該在事件迴圈執行期間呼叫。它可以被多次呼叫。

call_later(delay, callback, *args)。為callback(*args)安排延遲大約delay秒後呼叫,一旦,除非被取消了。返回一個Handler物件代表回撥函式,Handler物件的cancel()方法常用來取消回撥函式。

call_repeatedly(interval, callback, **args)。和call_later()類似,但是會在每個interval秒中重複呼叫回撥函式,直到返回的Handler被取消。第一次呼叫是在interval秒內。

call_soon(callback, *args)。類似call_later(0, callback, *args)。

call_soon_threadsafe(callback, *args)。類似call_soon(callback, *args),但是當事件迴圈在阻塞等待IO的時候在另外的執行緒呼叫,事件迴圈的阻塞會被取消。這是唯一安全地從另外的執行緒呼叫的方法。(要在一個執行緒安全的方式去延遲一段時間排程回撥函式,你可以使用ev.call_soon_threadsafe(ev.call_later,when,callback,*args)。)但它在訊號處理器中呼叫並不安全(因為它可以使用鎖)。

add_signal_handler(sig, callback, *args)。無論什麼時候接收到訊號 ``sigis , callback(*args)會被安排呼叫。返回一個能來取消訊號回撥函式的Handler。(取消返回的處理器回導致在下個訊號到來的時候呼叫remove_signal_handler()。優先明確地呼叫remove_signal_handler()。)為相同的訊號定義另外一個回到函式來替代之前的handler(每個訊號只能啟用一個handler)。sig引數必需是一個在訊號模組裡定義的有效的訊號值。如果訊號不能處理,這會丟擲一個異常:如果它不是一個有效的訊號或者如果它是一個不能捕獲的訊號(例如SIGKILL),會丟擲ValueError。如果這個特別的事件迴圈例項不能處理訊號(因為訊號是每個處理器的全域性變數,只有在主執行緒的事件迴圈才能處理這些訊號),它會丟擲RuntimeError。

remove_signal_handler(sig)。為訊號sig移除handler,當有設定的時候。丟擲和add_signal_handler()一樣的異常(除了在不能不錯的訊號時返回False代替丟擲RuntimeError)。如果handler移除成功,返回True,如果沒有設定handler則返回False。

一些符合標準介面返回Future的方法:

wrap_future(future)。 這裡需要在PEP 3148 描述的Future (例如一個concurrent.futures.Future的例項) 同時返回一個相容事件迴圈的Future (例如, 一個tulip.Future例項)。

run_in_executor(executor, callback, *args)。安排在一個執行器中呼叫callback(*args) (請看 PEP 3148)。返回的Future的成功的結果是呼叫的返回值。 這個方法等價於wrap_future(executor.submit(callback, *args))。 如果沒有執行器,則會是也能夠一個預設為5個執行緒的ThreadPoolExecutor。

set_default_executor(executor). 設定一個被run_in_executor()使用的預設執行器。

getaddrinfo(host, port, family=0, type=0, proto=0, flags=0). 類似socket.getaddrinfo()函式,但是返回一個Future。Future的成功結果為一列與socket.getaddrinfo()的返回值有相同格式資料。 預設的實現透過run_in_executor()來呼叫socket.getaddrinfo(),但其他實現可能會選擇使用他們自己的DNS查詢。可選引數必需是指定的關鍵字引數。

getnameinfo(sockaddr, flags=0). 類似socket.getnameinfo(),但返回一個Future。 Future的成功的結果將會是一個(host, port)的陣列。 與forgetaddrinfo()有相同的實現。

create_connection(protocol_factory, host, port, **kwargs). 使用給定的主機和埠建立一個流連結。這會建立一個依賴Transport的實現來表示連結, 然後呼叫protocol_factory()來例項化(或者取回)使用者的Protocol實現, 然後把兩者繫結到一起。(看下面對Transport和Protocol的定義。) 使用者的Protocol實現透過呼叫無引數(*)的protocol_factory()來建立或者取回。返回值是Future,它的成功結果是(transport, protocol)對; 如果有錯誤阻止建立一個成功的連結,Future會包含一個適合的異常集。注意,當Future完成的適合,協議的connection_made()方法不會呼叫;那會發生在連結握手完成的適合。

(*) 沒有要求protocol_factory是一個類。如果你的協議類需要定義引數傳遞到建構函式,你可以使用lambda或者functool.partial()。你也可以傳入一個之前構造好的Protocol例項的lambda。

可選關鍵引數:

family,proto,flags:地址簇,協議, 和混合了標誌的引數傳遞到getaddrinfo()。這些全是預設為0的。((socket型別總是SOCK_STREAM。)

ssl:傳入True來建立一個SSL傳輸(透過預設一個無格式的TCP來建立)。或者傳入一個ssl.SSLConteext物件來過載預設的SSL上下文物件來使用。  

start_serving(protocol_factory, host, port, **kwds)。進入一個接收連線的迴圈。返回一個Future,一旦迴圈設定到服務即完成;它的返回值為是None。每當接收一個連結,無引數(*)的protocol_factory被呼叫來建立一個Protocol,一個代表了連結網路端的Transport會被建立,以及兩個物件透過呼叫protocol.connection_made(transport)繫結到一起。

(*)看上面對create_connection()的補充說明。 然而, 因為protocol_factory()只會在每個新進來的連線呼叫一次,所以推薦在它每次呼叫的時候返回一個新的Protocol物件。

可選關鍵引數:

family,proto,flags:地址簇,協議, 和混合了標誌的引數傳遞到getaddrinfo()。這些全是預設為0的。((socket型別總是SOCK_STREAM。)

補充: 支援SSL嗎? 我並不知道怎樣來非同步地支援(SSL),我建議這需要一個證照。

補充:也許可以使一個Future的結果物件能夠用來控制服務迴圈,例如停止服務,終止所有的活動連線,以及(如果支援)調整積壓(的服務)或者其他引數?它也可以有一個API來查詢活動連線。另外,如果迴圈由於錯誤而停止服務,或者如果不能啟動,則返回一個僅僅完成了的Future(子類?)?取消了它可能會導致停止迴圈。

補充:一些平臺可能沒興趣實現所有的這些方法, 例如 移動APP就對start_serving()不太感興趣。 (儘管我的iPad上有一個Minecraft的伺服器...)

以下這些註冊檔案描述符的回撥函式的方法不是必需的。如果沒有實現這些方法,訪問這些方法(而不是呼叫它們)時會返回屬性錯誤(AttributeError)。預設的實現提供了這些方法,但是使用者一般不會直接用到它們,只有傳輸層獨家使用。同樣,在 Windows 平臺,這些方法不一定實現,要看到底是否使用了 select 或 IOCP 的事件迴圈模型。這兩個模型接收整型的檔案描述符,而不是 fileno() 方法返回的物件。檔案描述符最好是可查詢的,例如,磁碟檔案就不行。

add_reader(fd, callback, *args). 在檔案描述符 fd 準備好可以進行讀操作時呼叫指定的回撥函式 callback(*args)。返回一個處理函式物件,可以用來取消回撥函式。注意,不同於 call_later(),這個回撥函式可以被多次呼叫。在同一個檔案描述符上再次呼叫 add_reader() 將會取消之前設定的回撥函式。注意:取消處理函式有可能會等到處理函式呼叫後。如果你要關閉 fd,你應該呼叫 remove_reader(fd)。(TODO:如果已經設定了處理函式,丟擲一個異常)。

add_writer(fd, callback, *args).  類似 add_reader(),不過是在可以寫操作之前呼叫回撥函式。

remove_reader(fd). 為檔案描述符 fd 刪除已經設定的讀操作回撥函式。如果沒有設定回撥函式,則不進行操作。(提供這樣的替代介面是因為記錄檔案描述符比記錄處理函式更方便簡單)。刪除成功則返回 True,失敗則返回 False。

remove_writer(fd).  為檔案描述符 fd 刪除已經設定的寫操作回撥函式。

未完成的:如果一個檔案描述符裡面包含了多個回撥函式,那應該怎麼辦呢?目前的機制是替換之前的回撥函式,如果已經註冊了回撥函式則應該招聘異常。

接下來下面的方法在socket的非同步I/O中是可選的。他們是替代上面提到的可選方法的,目的是在Windows的傳輸實現中使用IOCP(如果事件迴圈支援)。socket引數必需是唔阻塞socket。

sock_recv(sock, n)。從套接字sock中接收位元組。返回一個Future,Future在成功的時候會是一個位元組物件。

sock_sendall(sock, data)。傳送位元組資料到套接字sock。返回一個Future,Future的結果在成功後會是None。(補充:讓它去模擬sendall()或send()會更好嗎?但是我認為sendall()——也許它扔應該命名為send()?)

sock_connect(sock, address)。連線到給定的地址。返回一個Future,Future的成功結果是None。

sock_accept(sock)。從socket中接收一個連結。這socket必需在監聽模式以及繫結到一個定製。返回一個Future,Future的成功結果會是一個(conn,peer)的陣列,conn是一個已連線的無阻塞socket以及peer是對等的地址。(補充:人們告訴我這個API風格對於高水平的伺服器是很慢的。所以上面也有start_sering()。我們還需要這個嗎?)

補充:可選方法都不是太好的。也許這些都是需要的?它仍然依賴於平臺的更有效設定。另外的可能是:文件標註這些“僅提供給傳輸”,然後其他的“可提供給任何的情況”。

回撥順序


當在同一時間排程兩個回撥函式時,它們會按照註冊的順序去執行。例如:

ev.call_soon(foo)

ev.call_soon(bar)

保證foo()會在bar()執行。

如果使用call_soon(),即使系統時鐘要逆行,這個保證還是成立的。這同樣對call_later(0,callback,*args)有效。然而,如果在系統時鐘要逆行下,零延遲地使用call_later(),那就無法得到保證了。(一個好的事件迴圈實現應該使用time.monotonic()來避免系統時鐘逆行的情況導致的問題。參考 PEP 418 。)

上下文


所有的事件迴圈都有上下文的概念。對於預設的事件迴圈實現來說,上下文就是一個執行緒。一個事件迴圈實現應該在相同的上下問中執行所有的回撥。一個事件迴圈實現應該在同一時刻只執行一個回撥,所以,回撥要負責保持與相同事件迴圈裡排程的其他回撥自動互斥。

異常


在Python裡有兩類異常:從Exception類到處的和從BaseException匯出的。從Exception匯出的異常通常能適當地被捕獲和處理;例如,異常會透過Future傳遞,以及當他們在一個回撥了出現時,會被記錄和忽略。

然而,從BaseException到處的異常從來不會被捕獲到,他們通常帶有一個錯誤回溯資訊,同時導致程式終止。(這類的例子包括KeyboardInterrupt和SystemExit;如果把這些異常與其他大部分異常同樣對待,那時不明智的)。

Handler類


有各樣註冊回撥函式的方法(例如call_later())都會返回一個物件來表示註冊,改物件能夠用來取消回撥函式。儘管使用者從來不用去例項化這個類,但還是想要給這個物件一個好的名字:Handler。這個類有一個公用的方法:

cancel(). 嘗試取消回撥函式。 補充:準確的規範。

只讀的公共屬性:

callback。 要被呼叫的回撥函式。

args。呼叫回撥函式的引數陣列。

cancelled。如果cancel()表呼叫了,它的值為True。


要注意的是一些回撥函式(例如透過call_later()註冊的)意味著只會被呼叫一次。其他的(如透過add_reader()註冊的)意味著可以多次被呼叫。

補充:一個呼叫回撥函式的API(是否有必要封裝異常處理)?它是不是要記錄自己被呼叫了多少次?也許這個API應該是_call_()這樣?(但它應該抑制異常。)

補充:當回撥函式在排程的時候有沒有一些公共的屬性來記錄那些實時的值?(因為這需要一些方法來把它儲存到堆裡面的。)

Futures


ulip.Future 特意設計成和 PEP 3148中的 concurrent.futures.Future 類似,只是有細微的不同。這個PEP中談及 Future 時,都是指 tulip.Future,除非明確指定是  concurrent.futures.Future。tulip.Future支援的公開API如下,同時也指出了和 PEP 3148 的不同:

cancel().  如果該 Future 已經完成(或者被取消了),則返回 False。否則,將該 Future 的狀態更改成取消狀態(也可以理解成已完成),排程回撥函式,返回 True。

cancelled(). 如果該 Future 已經被取消了,返回 True。

running(). 總是返回False。和 PEP 3148 不同,這裡沒有 running 狀態。

done(). 如果該Future已經完成了,返回True。注意:取消了的Future也認為是已經完成了的(這裡和其他地方都是這樣)。

result(). 返回 set_result() 設定的結果,或者返回 set_exception() 設定的異常。如果已經被取消了,則丟擲CancelledError。和 PEP 3148 不同,這裡沒有超時引數,並不等待。如果該Future尚未完成,則丟擲一個異常。

exception(). 同上,返回的是異常。

add_done_callback(fn).  新增一個回撥函式,在Future完成(或者被取消)時執行。如果該Future已經完成(或者被取消),透過 call_soon() 來排程回撥函式。不同於 PEP 3148,新增的回撥函式不會立即被呼叫,且總是會在呼叫者的上下文中執行。(典型地,一個上下文是一個執行緒)。你可以理解為,使用 call_soon() 來呼叫該回撥函式。注意:新增的回撥函式(不同於本PEP其他的回撥函式,且忽略下面"回撥風格(Callback Style)"小節的約定)總會接收到一個 Future 作為引數,且這個回撥函式不應該是 Handler 物件。

set_result(result). T該Future不能處於完成(或者取消)狀態。這個方法將使當前Future進入完成狀態,並準備呼叫相關的回撥函式。不同於 PEP 3148:這是一個公開的API。

set_exception(exception).  同上,設定的是異常。

內部的方法 set_running_or_notify_cancel() 不再被支援;現在已經沒有方法直接設定成 running  狀態。

這個PEP定義了以下的異常:

InvalidStateError. 當呼叫的方法不接受這個 Future 的狀態時,將會丟擲該異常(例如:在一個已經完成的 Future 中呼叫set_result() 方法,或者在一個未完成的 Future 中呼叫 result()方法)。

InvalidTimeoutError. 當呼叫 result() 或者 exception() 時傳遞一個非零引數時丟擲該異常。

CancelledError.  concurrent.futures.CancelledError 的別名。在一個已經取消的 Future 上面呼叫 result() 或 exception() 方法時丟擲該異常。

TimeoutError. concurrent.futures.TimeoutError 的別名。有可能由 EventLoop.run_until_complete() 方法丟擲。

建立一個 Future 時將會與預設的事件迴圈關聯起來。(尚未完成的:允許傳遞一個事件迴圈作為引數?)。

concurrent.futures 包裡面的 wait() 和 as_completed() 方法不接受 tulip.Future 物件作為引數。然而,有類似的 API tulip.wait() 和 tulip.as_completed(), 如下所述。

在子程式(coroutine)中可以將 tulip.Future 物件應用到 yield from 表示式中。這個是透過 Future 中的 __iter__() 介面實現的。請參考下面的“子程式和排程器”小節。

當 Future 物件被回收時,如果有相關聯的異常但是並沒有呼叫 result() 、 exception() 或 __iter__() 方法(或者說產生了異常但是還沒有丟擲),那麼應該將該異常記錄到日誌中。TBD:記錄成什麼級別?

將來,我們可能會把 tulip.Future 和 concurrent.futures.Future 統一起來。例如,為後面個物件新增一個 __iter__() 方法,以支援 yield from 表示式。為了防止意外呼叫尚未完成的 result() 而阻塞事件迴圈,阻塞機制需要檢測當前執行緒是否存在活動的事件迴圈,否則丟擲異常。然而,這個PEP為了儘量減少外部依賴(只依賴 Python3.3),所以目前將不會對 concurrent.futures.Future 作出改變。

傳輸層


傳輸層是指基於 socket 或者其他類似的機制(例如,管道或者SSL連線)的抽象層。這裡的傳輸層深受 Twisted 和 PEP 3153 的影響。使用者很少會直接實現或者例項化傳輸層,事件迴圈提供了設定傳輸層的相關方法。

傳輸層是用來和協議一起工作的。典型的協議不關心底層傳輸層的具體細節,而傳輸層可以用來和多種的協議一起工作。例如,HTTP 客戶端實現可以使用普通的 socket 傳輸層,也可以使用SSL傳輸層。普通 socket 傳輸層可以與 HTTP 協議外的大量協議一起工作(例如,SMTP, IMAP, POP, FTP, IRC, SPDY)。

大多數連線有不對稱特性:客戶端和伺服器通常有不同的角色和行為。因此,傳輸層和協議之間的介面也是不對稱的。從協議的視角來看,傳送資料透過呼叫傳輸層物件的 write() 方法來完成。write() 方法將資料放進緩衝區後立即返回。在讀取資料時,傳輸層將充當一個更主動的角色:當從 socket(或者其他資料來源) 接到資料後,傳輸層將呼叫協議的 data_received() 方法。

傳輸層有以下公開方法:

write(data). 寫資料。引數必須是一個 bytes 物件。返回 None。傳輸層可以自由快取 bytes 資料,但是必須確保資料傳送到另一端,並且維護資料流的行為。即:t.write(b'abc'); t.write(b'def') 等價於 t.write(b'abcdef'),也等價於:

t.write(b'a')

t.write(b'b')

t.write(b'c')

t.write(b'd')

t.write(b'e')

t.write(b'f')

writelines(iterable). 等價於:

for data in iterable:

   self.write(data)

write_eof(). 關閉寫資料端的連線,將不再允許呼叫 write() 方法。當所有緩衝的資料傳輸之後,傳輸層將向另一端發訊號,表示已經沒有其他資料了。有些協議不支援此操作;那樣的話,呼叫 write_eof() 將會丟擲異常。(注意:這個方法以前叫做 half_close(),除非你明確知道具體含義,這個方法名字並不明確表示哪一端會被關閉。)

can_write_eof().  如果協議支援 write_eof(),返回 True ;否則返回 False。(當 write_eof() 不可用時,有些協議需要改變相應的行為,所以需要這個方法。例如,HTTP中,為了傳送當前大小未知的資料,通常會使用 write_eof() 來表示資料傳送完畢。但是,SSL 不支援這種行為,相應的HTTP協議實現需要使用分段(chunked)編碼。但是如果資料大小在傳送時未知,適用於兩種情況的最好方案是使用 Content-Length 頭。)

pause(). 暫停傳送資料,直接呼叫了 resume() 方法。在 pause() 呼叫,再到呼叫 resume() 之間,不會再呼叫協議的 data_received() 方法。在 write()  方法中無效。

resume(). 使用協議的 data_received() 重新開始傳輸資料。

close(). 關閉連線。在所有使用 write() 緩衝好的資料傳送完畢之前,不會關閉連線。連線關閉後,協議的 data_received() 方法不會再被呼叫。當所有緩衝的資料傳送完畢後,將會用 None 作為引數呼叫協議的 connection_lost() 方法。注意:這個方法不確保會呼叫上面所有的方法。

abort(). 中斷連線。所有在緩衝區內尚未傳輸的資料都會被丟棄。不久後,協議的 connection_lost() 將會被呼叫,傳入一個 None 引數。(待定:在 close(), abort() 或者另一端的關閉動作中,對 connection_lost() 傳入不同的引數? 或者新增一個方法專門用來查詢這個? Glyph 建議傳入不同的異常)

尚未完成的:提供另一種流量控制的方法:傳輸層在緩衝區資料成為負擔的情況下有可能暫停協議層。建議:如果協議有 pause() 和 resume() 方法的話,允許傳輸層呼叫它們;如果不存在,則協議不支援流量控制。(對 pause() 和 resume(),可能協議層和傳輸層使用不同的名稱會好一點?)

協議 (Protocols)


協議通常和傳輸層一起配合使用。這裡提供了幾個常用的協議(例如,幾個有用的HTTP客戶端和伺服器實現),大多數協議需要使用者或者第三方庫來實現。

一個協議必須實現以下的方法,這些方法將被傳輸層呼叫。這些回撥函式將被事件迴圈在正確的上下文中呼叫(參考上面的上下文("Context")小節)。

connection_made(transport). 意味著傳輸層已經準備好且連線到一個實現的另一端。協議應該將傳輸層引用作為一個變數儲存起來(這樣後面就可以呼叫它的 write() 及其他方法),也可以在這時傳送握手請求。

data_received(data). 傳輸層已經從讀取了部分資料。引數是一個不為空的的 bytes 物件。這個引數的大小並沒有明確的限制。 p.data_received(b'abcdef') 應該等價於下面的語句:

p.data_received(b'abc')

p.data_received(b'def')

eof_received(). 在另一端呼叫了 write_eof() 或者其他等價的方法時,這個方法將被呼叫。預設的實現將呼叫傳輸層的 close() 方法,close() 方法呼叫了協議中的 connection_lost() 方法。

connection_lost(exc). 傳輸層已經關閉或者中斷了,另一端已經安全地關閉了連線,或者發生了異常。在前三種情況中,引數為 None;在發生了異常的情況下,引數是導致傳輸層中斷的異常。(待定:是否需要區分對待前三種情況?)

這裡是一個表示了呼叫的順序和多樣性的圖:

connection_made()-- exactly once

data_received()-- zero or more times

eof_received()-- at most once

connection_lost()-- exactly once

補充: 討論使用者的程式碼是否要做一些事情保證協議和傳輸協議不會被過早地GC(垃圾回收)。

回撥函式風格


大部分介面採取回撥函式也採取位置引數。舉例來說,要安排foor("abc",42)馬上呼叫,你可以呼叫ev.call_soon(foo,"abc",42)。要計劃呼叫foo(),則使用ev.call_soon(foo)。這種約定大大地減少了需要典型的回撥函式程式設計的小lambda表示式的數量。

這種約定明確的不支援關鍵字引數。關鍵字引數常用來傳遞可選的關於回撥函式的額外資訊。這允許API的優雅的改革,不用擔心是否一個關鍵字在某些地方被一個呼叫者宣告。如果你有一個必需使用關鍵字引數呼叫的回撥函式,你可以使用lambda表示式或者functools.partial。例如:

ev.call_soon(functools.partial(foo, "abc", repeat=42))


選擇一種事件迴圈的實現方式

待完成。(關於使用select/poll/epoll,以及如何改變選擇。屬於事件迴圈策略)

協程和排程器


這是一個獨立的頂層部分,因為它的狀態與事件迴圈介面不同。協程是可選的,而且只用回撥的方式來寫程式碼也是很好的。另一方面,只有一種排程器/協程的API實現,如果你選擇了協程,那就是你唯一要用的。

協同程式


一個協同程式是遵循一下約定的生產者。為了良好的文件的目的,所有的協同程式應該用@tulip.coroutine來修飾,但這並沒嚴格要求。

協同程式使用在 PEP 380 裡介紹的yield from語法啦代替原始的yield語法。

這個“協同程式”的詞和“生產者”的含義類似,是用來描述兩個不同(儘管是有關係)的概念。

定義了協同程式的函式(使用tulip.coroutine修飾定義的一個函式)。如果要消除歧義的話,我們可以稱這個函式為協同程式函式(coroutine function)。

透過一個協同函式獲得物件。這個物件代表了一個最終會完成的計算或者I/O操作(通常兩者都有)。我們可以稱這個物件為協同程式物件(coroutine object)來消除歧義。


協程能做的事情:

結果= 從future使用yield-- 直到future完成,掛起協程,然後返回future的結果,或者丟擲它要傳遞的異常。

結果 = 從coroutine使用yield--等待另外的協程產生結果 (或者丟擲一個要傳遞的異常)。這協程的異常必須是對另外一個協同程式的呼叫。

返回結果-- 為使用yield from表示式等待結果的協程返回一個結果。

丟擲異常-- 在協程裡為使用yield from表示式等待的(程式)丟擲一個異常。

呼叫一個協程不會馬上執行它的程式碼——它僅僅是一個生產者,同時透過呼叫而返回的協程確實只是一個生產者物件,它在你迭代它之前不會做任何事情。對於協程來說,有兩種基本的方法來讓它開始執行:從別的協程裡呼叫yield(假設另外的協程已經在執行了!),或者把它轉換為一個Task(看下面)。

協程只有在事件迴圈在執行時才能執行。

等待多個協同程式


有兩個類似wait()和as_completed(),在包concurrent.futures裡的API提供來等待多個協同程式或者Future:

tulip.wait(fs, timeout=None, return_when=ALL_COMPLETED)。 這是一個由fs提供等待Future或者其他協同程式完成的協同程式。協同程式引數會封裝在Task裡(看下面)。這方法會返回一個Future,Future的成功結果是一個包含了兩個Future集的元組(做完(done),掛起(pending)),done是一個原始Future(或封裝過的協同程式)的集合表示完成(或取消),以及pending表示休息,例如還沒完成(或者取消)。可選引數timeout和return_when與concurrent.futures.wait()裡的引數都有同樣的意思和預設值:timeout,如果不是None,則為全部的操作指定一個timeout;return_when,指定什麼時候停止。常量FIRST_COMPLETED,FIRST_EXCEPTION,ALL_COMPLETED使用相同的值定義同時在 PEP 3148裡有相同的意思:

ALL_COMPLETED(default): 等待,知道所有的Future處理了或者完成了 (或者直到超時發生)。

FIRST_COMPLETED: 等待,知道至少一個Future做好了或者取消(或者直到超時發生)。

FIRST_EXCEPTION: 等待,知道至少一個Future由於異常而做好(非取消) (這種從過濾器中排除取消的Future是很神奇的,但PEP 3148 就用這種方法裡做了。)

tulip.as_completed(fs, timeout=None). 返回一個值為Future的迭代器; 等待成功的值,直到下一個Future或者協同程式從fs中完成都處於等待狀態, 同時返回它自己的結果 (或者丟擲它的異常)。可選引數timeout與concurrent.futures.wait()裡的引數都有同樣的意思和預設值: 當存在超時的時候, 下一個由迭代器返回的Future在等待的時候會丟擲異常TimeoutError。 使用列子:

for f in as_completed(fs):

   result = yield from f  # May raise an exception.

   # Use result.


任務(Task)


任務(Task)是一個管理獨立執行子程式的物件。Task介面和Future介面是一樣的。如果子程式完成或者丟擲異常,那麼與之關聯的任務也就完成了。返回的結果就是相應任務的結果,丟擲的異常就是相應任務的異常。

如果取消一個尚未完成的任務,將會阻止關聯的子程式繼續執行。在這種情況下,子程式將會接收到一個異常,以更好地處理這個取消命令。當然,子程式不一定要處理這個異常。這種機制透過呼叫生成器標準的  close() 方法,這個在 PEP 342 中有描述。

任務對於子程式間的互動以及基於回撥的框架(例如 Twisted)也很有用。將一個子程式轉化成任務之後,就可以將回撥函式加到任務裡。

你可能會問,為什麼不將所有子程式都轉化成任務? @tulip.coroutinedecorator 可以實現這個任務。這樣的話,如果一個子程式呼叫了另外的子程式(或者其他複雜情況),那麼整個程式將會變得相當慢。在複雜情況下,保持子程式比轉換成任務要快得多。

排程器


排程器沒有公開的介面。你可以使用 future 和 task 的 yield 和排程器進行互動。實際上,排程器並沒有一個具體的類實現。透過使用事件迴圈的公共介面,Future 和 Task 類表現了排程器的行為。所以即使換了第三方的事件迴圈實現,排程器特性也可以使用。

睡眠


尚未完成的:yield sleep(秒). 可以用 sleep(0) 來掛起並查詢I/O。

協同程式與協議


使用協同程式去實現協議的最好方法坑農是使用一個流快取,這快取使用data_received()來填充資料,同時能夠使用像read(n)和readline()等返回一個Future的方法來非同步讀取資料。當連結關閉時,read()方法應該返回一個Future,這Future的結果會是'',或者如果connection_closed()由於異常而被呼叫,則丟擲一個異常。

要寫入資料的話,在transport上的write()(及同型別的)方法能夠使用——這些不會返回一個Future。應該提供用於設定和在呼叫了connection_made()時開始協同程式的一個標準的協議實現。

補充:更多的規範。

取消


補充。當一個任務被取消了,它的協同程式會在它從排程程式中放棄的任何一個地方中看到異常(例如可能在操作中放棄)。我們需要講清楚要丟擲什麼異常。

再次補充:超時。

已知問題


除錯的API? 例如 一些能夠記錄大量材料的或者是記錄不常用條件的 (就像佇列填充比排空快)或者甚至回撥函式耗費大量時間...

我們需要內省的API嗎?例如請求讀回撥函式返回一個檔案描述符。或者當下一個排程的(回撥函式)被呼叫時。或者由回撥函式註冊了的一些檔案描述符。

傳輸可能需要一個方法來試著返回socket的地址(和另外的方法返回對等的地址)。儘管這是依賴於socket的型別,同時這並不總是一個socket;然後就應該返回None。(作為選擇,可以有個方法來返回socket自身——但可以想到一個沒有使用socket來實現IP連結,那它應該怎麼做呢?)

需要處理os.fokd()。(這可能在Tulip的情況下會上升到選擇器類。)

也許start_serving()需要一個方法來傳入到一個當前的socket(例如gunicorn就需要這個)。create_connection()也有同樣的問題。

我們也許會介紹一些明確的鎖,儘管使用起來有點痛苦,因為我們不能使用with鎖:阻塞語法(因為要等待一個鎖的話,我們就要用yield from了,這是with語句不能做到的)。

是否要支援資料包協議、連結。可能要更多的套接字I/O方法,例如sock_sendto()和sock_recvfrom()。或者使用者客戶自己編寫(這不是火箭科學)。有它的理由去覆蓋write(),writelines(),data_received()到一個單一的資料包?(Glyph推薦後者。)然後用什麼來代替write()?最後,我們需要支援無連線資料包協議嗎?(這意味著要封裝sendto()和recvfrom()。)

我們可能需要API來控制各種超市。例如我們可能想限制在解析DNS、連結、SSL握手、空閒連結、甚至是每個會話裡消耗的時間。也許有能充分的加入timeout的關鍵字引數到一些方法裡,和其他能夠巧妙地透過call_later和Task.cancel()來實現超時。但可能有些方法需要預設的超時時間,同時我們可能想改變這種規範的全域性操作的預設值。(例如,每個事件迴圈)。

一個NodeJS風格的事件觸發器?或者把這個作為一個單獨的教程?這其實在使用者空間做是足夠容易的,儘管可能放在標準化裡好一點(看https://github.com/mnot/thor/blob/master/thor/events.py和https://github.com/mnot/thor/blob/master/doc/events.md 舉的例子。)


參考文獻


PEP 380 來自於TBD:Greg Ewing的教程描述yield的語義。

PEP 3148 描述concurrent.futures.Future.

PEP 3153,雖然被拒絕了, 但很好的描述了分離傳輸和協議的需要。

Tulip repo: http://code.google.com/p/tulip/

Nick Coghlan在一些背景下寫的一個很好的部落格條目,關於非同步I/O不同處理、gevent、以及怎樣使用future就像wihle、for和with的概念的思想, : http://python-notes.boredomandlaziness.org/en/latest/pep_ideas/async_programming.html

TBD: 有關Twisted、Tornado、ZeroMQ、pyftpdlib、libevent、libev、pyev、libuv、wattle等的參考

鳴謝


除了PEP 3153之外, 受到的影響包括 PEP 380 and Greg Ewing的yield from的教程, Twisted, Tornado, ZeroMQ, pyftpdlib, tulip (作者的企圖是想把這些全部綜合起來), wattle (Steve Dower的相反的提議), 2012年9月到12月在python-ideas上大量討論, 一個與Steve Dower和 Dino Viehland在Skype上的會話, 和Ben Darnell的電子郵件交流, 一個Niels Provos的聽眾 (libevent原始作者),兩場和幾個Twisted開發者的面對面會議, 包括了 Glyph、Brian Warner、David Reid、 以及Duncan McGreggor。同樣的,作者前期在為Google App Engine的NDB庫的非同步支援也有重要的影響。


相關文章