An introduction to asynchronous Python原文
作者: Jake Edge 于2017年6月28日
在他的PyCon 2017演講中,Miguel Grinberg希望通過Python來介紹異步編程給完全的初學者。 有很多關于異步Python的討論,特別是隨著asyncio模塊的出現(xiàn) 。但是有多種方法可以創(chuàng)建異步Python程序,其中許多方法已經可用了很長時間。 在演講中,Grinberg從這些解決方案的復雜性中退了一步,從更高層次來看異步處理方式。
他開始談到,盡管他在基于Python的Flask 網(wǎng)頁微型框架上做了大量的工作,這個演講不會是關于Flask的。他寫的是Flask Mega-Tutorial (一本關于Flask的書 ),但是在演講中他會提到不到十次,這是他令人羨慕的壯舉。他還開發(fā)了一個用于Socket.IO的Python服務器 ,它開始于前面說的 “框架”,但沒料到它 “自己的生活” 已經開始了。
他詢問參加者是否聽到有人說 “異步會使你的代碼更快速” 。如果是這樣,他說,他的演講會解釋為什么人們這么說。 他開始簡單地定義 “異步(async)” (因為 “asynchronous” 通常被縮短)。 它是并行編程的一種方法,這意味著一次執(zhí)行很多操作。他在這里提到的不僅僅是asyncio ,因為有許多方法可以讓Python一次性執(zhí)行多個操作。
然后他回顧了這些機制。首先是多個進程,其中操作系統(tǒng)(OS)執(zhí)行多任務的所有工作。 在CPython(參考Python實現(xiàn))中,這是使用系統(tǒng)中所有核的唯一方法。 另一種同時做多個事情的方式是使用多線程,這也是操作系統(tǒng)處理多任務的一種方式,但Python的全局解釋器鎖(Global Interpreter Lock,GIL)會阻止多核并發(fā)。另一方面,異步編程不需要OS參與。 只有有一個進程和線程,但該程序可以一次完成多項操作。 他問: “訣竅是什么?”
象棋
他轉向了一個現(xiàn)實世界的例子:一個象棋大師在象棋展,同時面對24個對手。 “在電腦殺死象棋之前,這些展覽定期進行,但他不確定是否現(xiàn)今還在。” 如果每個游戲需要大約30個成對移動來完成,如果連續(xù)玩(每個成對移動一分鐘),大師將需要12小時才能完成比賽。 但是,通過在每個游戲中依次進行動作,整個練習可以在一個小時內完成。 大師只是在一個棋盤上(在五秒鐘內)移動,然后繼續(xù)下一步,在大師返回之前(進行另外23個動作)后,讓對手有很多時間移動。 格林伯格說,那位大師在那個時候會 “讓大家玩好” 。
人們正在談論的異步編程就是 “這樣的快速” 。象棋大師沒有優(yōu)化玩的更快,只是工作安排得好,使他們不浪費時間做無謂的等待。 他說: “這是異步編程的完整秘密” ,他說: “這就是怎么運作的”。 在這種情況下,CPU就是象棋大師,它等待盡可能少的時間。
但與會者可能想知道如何只使用一個進程和一個線程來完成。 如何實現(xiàn)異步? 需要的一件事是,一種方法可以來暫停和恢復執(zhí)行函數(shù)。他們將在等待時掛起(suspend)且在等待結束時恢復(resume)。 這聽起來很難做,但在Python中有四種方法可以在不涉及操作系統(tǒng)的情況下進行。
他的第一個方法是回調函數(shù),這是 “顯而易見(gross)” ,他說。 如此顯而易見,事實上,他甚至沒有舉個例子。 另一個是使用生成器函數(shù) ,這是Python長期以來的一部分。最近的Python從3.5開始,具有async和await關鍵字 ,可以用于異步程序。 還有一個第三方軟件包, greenlet ,它有一個Python的C擴展,以支持掛起和恢復。
還需要有一件事情以支持異步編程:調度器,可以跟蹤掛起的函數(shù),并在正確的時間恢復它們。在異步世界中,該調度程序被稱為 “事件循環(huán)” 。 當函數(shù)暫停時,它將控制權返回給事件循環(huán),該循環(huán)找到另一個需要啟動或恢復的函數(shù)。 這不是一個新的想法; 它與Windows和macOS的舊版本中使用的 “合作多任務” 實際上是一樣的。
例子
Grinberg創(chuàng)建了一個使用一些不同機制的簡單 “hello world” 程序的例子 。 他在演講中并沒有講到他們的全部,也鼓勵觀眾看其余的部分。 他開始于一個簡單的同步示例,它具有在打印 “Hello” 和 “World!” 之間睡三秒鐘的功能。 如果他在一個循環(huán)中調用了十次,則需要30秒才能完成,因為每個函數(shù)都將背靠背運行。
然后他使用asyncio顯示了兩個例子。它們本質上是一樣的,但是一個使用@coroutine裝飾器(decorator)給函數(shù)且在函數(shù)體內使用yield from(生成器函數(shù)的風格),而另一個使用async def的函數(shù),并在函數(shù)體中await。 兩者都使用asyncio版本的sleep()函數(shù)在兩次print()調用之間休眠三秒鐘。 除了這些差異,還有一些樣板設置事件循環(huán)并從中調用函數(shù),這兩個函數(shù)具有與原始示例相同的核心代碼。非樣板差異是有意設計的; asyncio使代碼掛起和恢復的地方 “非常明確” 。
這兩個程序如下所示:
# async/await version
import asyncio
loop = asyncio.get_event_loop()
async def hello():
print('Hello')
await asyncio.sleep(3)
print('World!')
if __name__ == '__main__':
loop.run_until_complete(hello())
# @coroutine decorator version
import asyncio
loop = asyncio.get_event_loop()
@asyncio.coroutine
def hello():
print('Hello')
yield from asyncio.sleep(3)
print('World!')
if __name__ == '__main__':
loop.run_until_complete(hello())
運行程序給出了預期的結果(兩個字符串之間的三秒鐘),但是如果你在循環(huán)中包裝函數(shù)調用,它會變得更有趣。 如果循環(huán)十次迭代,結果將是十個 “Hello” 字符串,三秒鐘等待,然后十個 “World!” 字符串。
另外還有一些asyncio以外的示例,包括greenlet和Twisted。greenlet示例看起來與同步示例幾乎完全相同,只是使用不同的sleep() 。 那就是因為greenlet試圖使異步編程變得透明,但是隱藏這些差異可能是一個祝福也可能是詛咒,Grinberg說。
陷阱
在異步編程中有一些陷阱,人們總是會栽在這些事情上。如果有一個任務要求CPU使用量大,那么在進行計算時就不會做任何事情了。 為了讓其他事情發(fā)生,計算需要定期釋放CPU。這可以通過睡眠0秒來完成,例如(使用等待asyncio.sleep(0))。
然而,許多Python標準庫以阻塞方式編寫,因此套接字, 子進程和線程模塊(以及使用它們的其他模塊)以及諸如time.sleep()之類的簡單內容不能在異步程序中使用。 Grinberg說,所有的異步框架都為這些模塊提供了自己的非阻塞替換,但這意味著 “你必須重新學習如何做這些你已經知道如何做的事情” 。
Eventlet和gevent,它們都是基于greenlet的,它們都可以用來修補標準庫,使其與異步兼容,但這不是asyncio的功能。 它是一個不試圖隱藏程序異步性質的框架。asyncio希望您在設計和編寫代碼時考慮異步編程。
對照
他結束他的演講,比較了不同類別的進程,線程和異步。 所有這些技術都優(yōu)化了等待期; 進程和線程讓操作系統(tǒng)為他們做,而異步程序和框架為自己做。只有進程可以使用系統(tǒng)的所有內核,但線程和異步程序不能使用。 這導致一些人編寫程序,將每個核心的一個進程與線程和/或異步功能相結合,這可以很好地工作,他說。
可擴展性是 “有趣的” 。 運行多個進程意味著有多個Python副本,應用程序和所有在內存中使用的資源,所以在相當少的同時進程(數(shù)十個進程是可能的限制)后,系統(tǒng)將耗盡內存,Grinberg說。線程更輕巧,所以可以有更多的,甚至數(shù)百個。但異步程序是 “非常輕量級的” ,可以處理成千上萬個同時執(zhí)行的任務。
阻塞標準庫函數(shù)可以從進程和線程使用,但不能用于異步程序。GIL只會干擾線程,進程和異步可以和它共存就可以了。 不過,他指出,即使對于他經驗中的線程,GIL只有 “一些” 干擾; 當線程在I/O上被阻塞時,它們將不會保持GIL,所以操作系統(tǒng)將給予另一個線程CPU。
在這種比較中,沒有幾個能比async好。Grinberg說,Python的異步程序的主要優(yōu)點是它們允許的大規(guī)模擴展。 因此,如果您的服務器將處于超級忙碌狀態(tài)并處理大量同時發(fā)生的客戶端,則async可能會幫助您避免購買服務器而破產。 異步編程模型也可能由于其他原因而有吸引力,這是完全有效的,但嚴格按照處理優(yōu)勢,表明可縮放(scaling)是async真正獲勝的地方。
有關Grinberg的演講的YouTube視頻是可用的; 演講者甲板幻燈片(Speaker Deck slides)是相似的,但不同于他所使用的版本。
[我要感謝Linux基金會為去波特蘭參加PyCon提供的旅行援助。]