python异步_Python中的异步编程
Python部落(python.freelycode.com)組織翻譯,禁止轉(zhuǎn)載,歡迎轉(zhuǎn)發(fā)。
Quora的使命就是分享和增加全世界的知識,并且為了達到這個使命,我們不斷地推出改進來讓Quora對于我們的讀者和作者來說更快。在上一篇,縮短繪制時間,我們討論了對于客戶端性能的最新優(yōu)化,本文,我們將通過異步編程框架來討論服務(wù)器端性能的新發(fā)展。
為了同時優(yōu)化頁面加載和用戶操作的性能,我們積極使用緩存,以確保持續(xù)快速訪問常用數(shù)據(jù)。在Quora中,對于存儲在較慢的存儲系統(tǒng)如MySQL中的數(shù)據(jù),我們使用memcached作為主緩存層。例如,當我們呈現(xiàn)那些給答案投票者的名單時,需要從存儲層取回用戶ID到用戶名稱的映射。每個請求直接訪問MySQL的話,數(shù)據(jù)庫將很快超載,所以使用memcached存儲這個映射來替代。盡管為每一個用戶ID分配一個單獨的memcached請求會比查詢MySQL更快,但是通過一個單一的批量的多請求取回所有數(shù)據(jù)會更快。
因為網(wǎng)絡(luò)通常是memcached請求最昂貴的部分(在我們的環(huán)境中占總時間的80%以上),適當?shù)呐烤彺嬲埱髮τ诒3諵uora快速是很重要的。然而,如果開發(fā)者必須手動指定所有數(shù)據(jù)如何從memcached批量檢索,它將是單調(diào)易錯的。因此,我們已經(jīng)開發(fā)了一個抽象概念叫Asynq,它使開發(fā)者很容易編寫批量的緩存請求,如今它已經(jīng)開源。
Priming
在我們開發(fā)Asynq之前,使用一個叫做priming的方法來給memcached發(fā)送批量請求。每次開發(fā)者編寫訪問數(shù)據(jù)的函數(shù)時,也要編寫一個單獨的priming函數(shù)來指定該函數(shù)可以訪問的所有數(shù)據(jù)。例如,假設(shè)有一個檢索給定用戶ID列表并返回用戶標識列表的函數(shù),如下:
相應(yīng)的priming函數(shù)會看起來如下:
調(diào)用get_names的代碼將負責(zé)先調(diào)用prime_get_names,它將使用多請求從memecached獲取所有必要的數(shù)據(jù)。然后,該數(shù)據(jù)將被存儲在服務(wù)器的本地緩存中,所以當真正的函數(shù)(本例是get_names)運行時,它不會調(diào)用任何網(wǎng)絡(luò)請求!本質(zhì)上,每個prime函數(shù)表示的都是一個函數(shù)的依賴項,或者說它依賴的memecached鍵。
當我們需要調(diào)用一個緩存函數(shù)來決定從memcached中獲得什么額外數(shù)據(jù)時,priming會變得更復(fù)雜??紤]如下模板函數(shù):
在本例中,prime_get_upvoter_names 需要知道upvoter_uids,為了那些uids調(diào)用prime_name_of_user ,但由于upvoters_of_answer 被存儲了,它也必須被啟動。因此,相應(yīng)的prime函數(shù)將明顯更加復(fù)雜:
通過恰當?shù)貑⒂梦覀兯械哪P驼{(diào)用, 我們看到驚人的速度改進,因此,我們使用靜態(tài)分析工具來實施規(guī)則,即Quora上所有需要呈現(xiàn)的數(shù)據(jù)首先要被primed。然而,這些高速增長帶來了明顯的開發(fā)費用,因為實質(zhì)上開發(fā)者需要編寫(并調(diào)用)所有的模版函數(shù)兩次。隨著我們代碼庫的增長,priming變得冗余、難理解且易出錯。
Asynq
為了解決priming帶來的復(fù)雜性,我們創(chuàng)建一個叫做Asynq的框架,它在底層采用類似的方法,但是改進了API,把緩存請求集成到模板代碼本身。Asynq中,所有需要數(shù)據(jù)訪問的函數(shù)通過調(diào)度程序運行,調(diào)度程序記錄跟蹤它們的依賴項。當一個函數(shù)需要通過調(diào)用其它函數(shù)來獲取數(shù)據(jù)時,它不再控制調(diào)度程序,而是指示需要取回的數(shù)據(jù)。然后調(diào)度程序停止執(zhí)行該函數(shù)直到它解決了這個函數(shù)所有的依賴項。
Asynq中,get_names 函數(shù)早期看起來如下:
不需要額外的priming 函數(shù)——所有代碼都包含在模版函數(shù)本身。因此,開發(fā)者不僅不再需要編寫一個完全獨立的priming函數(shù),他們也不需要記憶每次模版函數(shù)被調(diào)用時調(diào)用priming函數(shù)。之前更復(fù)雜的get_upvoter_names在Asynq中也更簡單了:
可以把Async 函數(shù)理解成創(chuàng)建一個依賴關(guān)系圖:在它的第一個yield中,get_upvoter_names依賴于upvoters_of_answer的完成。類似地,upvoters_of_answer可能有它自己的依賴項。Asynq調(diào)度程序分解該依賴關(guān)系圖執(zhí)行async函數(shù),直到所有目前執(zhí)行的函數(shù)從memcached中獲取數(shù)據(jù)時阻塞。然后調(diào)度程序使用一個單獨的多請求從memcached取回數(shù)據(jù),并繼續(xù)執(zhí)行直到async函數(shù)完成。
假定我們有一個異步函數(shù)稱為model_call(),它有三個依賴項,每個依賴項都會讀取多個memcached鍵值。直接實現(xiàn)將會使用3個多請求(或者6個單一獲取),每個依賴項函數(shù)一個,而異步調(diào)度程序分解的依賴圖看起來如下所示:
我們異步編程的方法不同于Python中其它的異步庫如asyncio、Twisted、gevent和Tornado。這些庫側(cè)重于異步I/O,而Asynq卻側(cè)重于高效的批處理。例如,一個典型緩存使用memcached和asyncio的實現(xiàn)將分別解決緩存依賴項,因此每個memcached請求都會對memcached產(chǎn)生一個單獨的請求。在Asynq中,依賴項將會成批進入一個單獨的memcached多請求,這可以減少I/O阻塞的總時間。另外,Asynq允許函數(shù)被同步或異步調(diào)用(通過增加的.async屬性),而asyncio需要所有的async def函數(shù)被asyncio.get_event_loop()顯式調(diào)度。使用Asynq的批處理,我們花費很少時間阻塞在I/O,因此采用其它異步I/O庫對于我們來說不是優(yōu)先選擇。
與priming相比,Asynq提供一個更通用的、簡明的、有原則的方式來支持批處理。因為邏輯僅需要被實現(xiàn)一次,Asynq明顯比priming花費更少的開發(fā)費用。 減少priming的重復(fù)邏輯也提升了性能,正如我們所見,服務(wù)器端的速度取勝,由于我們遷移更多代碼庫從priming到Asynq。
遷移和學(xué)習(xí)
開發(fā)出第一個版本Asynq后,通過遷移代碼庫的幾個小部分從priming到Asynq,我們著手驗證我們的設(shè)計和實現(xiàn)。在這樣做的過程中,我們發(fā)現(xiàn)并修復(fù)了各種問題:Python2.7中,生成器不能返回值,所以以上代碼片段實際上在Python2.7中是無效的。在Asynq的第一個版本中,異步函數(shù)產(chǎn)生的最后一個值將會被解析為函數(shù)的返回值。然而,這意味著yield關(guān)鍵字意義的超載,這使得代碼很難閱讀。作為一個替代解決方案,我們從生成器中返回值——PEP 380中有介紹——從Python 3到Python 2.7,并且在代碼庫中必要的地方,我們目前正在使用Python 2.7的一個補丁版本。(Asynq也支持Python 2.7的未打補丁版本,通過使用一個result函數(shù),該函數(shù)拋出一個異常,該異常被解析為返回值。)
最初,使用@async()裝飾器使一個函數(shù)變?yōu)楫惒胶瘮?shù)完全改變了它的接口——所有的調(diào)用者不得不使用一個特殊語法來調(diào)用異步函數(shù)。這個決策使得priming和Asynq在我們的代碼庫中更難共存,由于所有的開發(fā)者需要意識到這種差異。為了修復(fù)這個問題,我們更新@async()裝飾器,給所有的異步函數(shù)增加了一個新的.async屬性,這樣直接調(diào)用一個裝飾器函數(shù)將仍舊返回結(jié)果。
起初測試異步函數(shù)具有挑戰(zhàn)性。在單元測試中,我們廣泛使用Python的mock模塊,但是很難模擬異步函數(shù),因為這樣做需要特殊處理.async的屬性并返回值。作為這個問題的解決方案,我們創(chuàng)建一個專用的模擬函數(shù),asynq.mock.patch,它自動負責(zé)模擬,這使得模擬異步函數(shù)更輕松。
實現(xiàn)上述改進后(和其它很多改進),我們決定將我們的代碼庫完全從priming遷移到Asynq。讓這兩種抽象概念同時存在于我們的代碼庫中不利于開發(fā)速度,因為工程師需要在兩種API中根據(jù)他們正在編輯哪種模型進行上下文切換。我們利用現(xiàn)有的靜態(tài)分析工具自動進行遷移,因此工程師只需要去核實腳本的輸出并做一點細小的改變,而不是手動遷移代碼。
在開始大規(guī)模遷移整個代碼庫之前,不同團隊的工程師完成一些較小的“演習(xí)”,作為細化我們自動遷移工具的手段,并建立精確的范圍估計。完成幾個演練后,我們由大約30個工程師(即我們工程師團隊的50%)進行了一次有協(xié)調(diào)性的遷移,在此期間僅用了4天時間,我們遷移了Quora代碼庫的15,000多行priming代碼。
如今,我們的整個代碼庫僅使用Asynq,并且服務(wù)器端開發(fā)速度更快且更不易出錯。
開源Asynq(和朋友們)
現(xiàn)在可以在GitHub和PyPI上獲得Asynq。你可以閱讀源碼或者通過pip install asynq安裝Asynq。和Asynq一起,我們也將QCore(Asynq的唯一依賴項)開源,它是一個助手集,用于整個Quora代碼庫,包括一個裝飾器框架,一個枚舉實現(xiàn),測試助手,和一個事件實現(xiàn)。Asynq和QCore同時兼容Python2.7和Python3,關(guān)于Asynq的更詳細文檔可以在GitHub倉庫查看。
未來,我們將繼續(xù)致力于使Quora對我們的用戶來說更快,使新特性對于我們的工程師來說更容易進行開發(fā)。我們目前正在招聘Platform工程師來開發(fā)像Asynq的核心框架和抽象概念,所以看看我們的職業(yè)頁面,如果你想和我們一起分享和增加全世界的知識!英文原文:https://engineering.quora.com/Asynchronous-Programming-in-Python?srid=hST?utm_source=mybridge&utm_medium=email&utm_campaign=read_more
譯者:蒲公英
總結(jié)
以上是生活随笔為你收集整理的python异步_Python中的异步编程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python的for语句写新的字符串_p
- 下一篇: python爬虫面试问题_Python爬