Python之tworoutine
本文主要描述了Python中的一種編碼風格,它允許輕鬆編寫混合同步和非同步的代碼。作為大型微波望遠鏡(包括南極望遠鏡)控制軟體的一部分,我們已經成功地在Tornado/Python 2.x技術棧下使用這種代碼。
不幸的是,Python 3.7中的體系結構變化與@tworoutine密不可分。為了促進對Python非同步生態系統的熱烈討論,我描述了它們對我們如此有用的原因。
介紹
Python中的非同步編程是由Twisted,Tornado和gevent等第三方庫開創的。官方event loop(事件循環)實現落在Python3.4中,並在Python3.7中得到了顯著擴展。像curio和trio這樣的新型非同步庫正在繼續提升非同步io的性能。
還有一些關於Python非同步生態系統的優秀文章。我不打算重述它們。但是,為了講得清楚,我將提供一些鏈接,為後面的內容奠定基礎。
PEP 3156--對非同步io的支持重新啟動:"asyncio"模塊(https://www.python.org/dev/peps/pep-3156/)
我不理解Python中的非同步io(http://lucumr.pocoo.org/2016/10/30/i-dont-understand-asyncio/)
aysnc/await在Python中如何工作(https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/)
控制Python中的非同步蠕變(https://hackernoon.com/controlling-python-async-creep-ec0a0f4b79ba)
其中,最後一個可能是最有趣的,因為它試圖解決我們在設計望遠鏡調整軟體時遇到的同樣問題:非同步和同步編程在Python中是不同的,但它對於自由混合使用它們卻是極其有用的。
為了混編同步和非同步代碼,這裡簡要描述了我們為調整望遠鏡編寫的代碼類型。
為望遠鏡編碼
我的日常工作包括CMB望遠鏡的工作,包括南極洲的南極望遠鏡和智利阿塔卡馬高原的西蒙斯陣列。
這些望遠鏡中的讀出電子設備是一系列軟體定義的無線電,其中有數千個發射器和接收器用於偏置和測量大爆炸的剩餘標記。這些無線電在數百個定製板中實現,定製板載有安裝在望遠鏡附近的板條箱中的FPGA,並由PC控制。這台PC啟動並運行系統,控制低溫冰箱,瞄準望遠鏡,並捕獲它產生的大量數據。
整個調優,控制和分析堆棧大量地使用Python,以及C,C++和VHDL。(我對我們依賴的許多開源社區感到無比感激,當我能夠以某種身份回饋時,這是一個很大的特權。)
可以想像,我們不只是將代碼直接部署到望遠鏡上。隨著望遠鏡本身的小規模安裝,從電路板或檯面上的兩個,到世界各地大學實驗室的低溫設備箱。在開發過程中,代碼可能在Jupyter筆記本或IPython shell中運行,可能只有一小部分電子設備或根本沒有。在這裡,互動式REPL會話用於原型演算法,探索數據,並嘗試新的調整和分析技術。
但是,對於在部署中有用的演算法,它需要大規模運行。這裡我們大量使用非同步代碼:與數百個電路板的命令交互非常適合非同步編碼風格。這導致產生以下工作流程:
設計流程,具有單獨的非同步/同步實現:
原型代碼,可能是同步的,主要專註於校對演算法或技術;
可以在互動式(ipython)環境中測試小規模部署的功能;
使用非同步方式重新編程實現演算法;
集成測試,優化和部署。
這種方法的優點:
在開發概念驗證時,開發人員能夠忽略性能並專註於他們試圖解決的問題(物理,儀器,低溫,電子)。
在原型設計過程中,互動式探索是非常有用的,同步代碼可以促進對IPython或Jupyter等環境的使用。
但是,此工作流程有三個主要缺點:
它很笨拙:它需要編寫和測試同步版本,然後批量轉移到非同步環境。這個工作流程很可能會循環往複很多次,因為在此過程中會引入許多錯誤。
儘管沒有擴展到望遠鏡級別的性能,同步版本永遠不會停止使用。在調試或試驗時,我們通常更願意擁有更簡單的語義,更可預測的控制流和更短的與同步調用相關的錯誤跟蹤。此外,它可以在REPL環境中方便地調用——如果望遠鏡正在運行並且我們需要進行一些快速手動調整,這是非常寶貴的。
它不可組合。多年來,我們已經建立了有用的調優和控制演算法庫,只要同步和非同步代碼保持不同,我們就無法在沒有兩個實現的情況下從較小的部分組成演算法。
要求開發人員在不同的編碼習慣下維護兩個版本(並希望保持版本同步)是通過要求熟練的勞動者做瑣碎的工作來解決技術缺陷;這通常是一個代價高昂的錯誤。(由於autoawait功能,非同步代碼的交互使用在IPython 7.0中變得更加容易。這個擴展解決了第二點而非第三點。)
相反,我們正在尋找一種自由混合非同步和同步編碼樣式的方法。
@tworoutine
什麼是@tworoutine?它是一個圍繞非同步函數的同步裝飾器。
你需要源代碼。你還需要nest_asyncio(https://github.com/erdewit/nest_asyncio)。
我們如何同步調用此函數?直接調用它吧!
這是怎麼回事? @tworoutine裝飾器返回一個類,其__call__方法是一個同步包裝器,它獲取一個事件循環並調用非同步代碼,阻塞直到它完成。因為我們希望同步調用方便且友好。
如果已經有一個事件循環在運行,那麼這段代碼相當有效(當然,除了是一個阻塞調用!)任何已在事件循環中排隊的非同步事件都可以繼續執行。在協程完成之前,僅阻止當前執行上下文。
同步調用非常多。我們如何非同步調用此函數?我們首先必須撤消或反轉包裝器並獲取一個返回協程的引用。
除了函數名稱周圍的反轉運算符外,這是普通的非同步代碼;除了操作符本身之外沒有額外的開銷。這是一個完整的示例,顯示事件循環中的混合編碼樣式:
這裡顯而易見的好處是,當我們懶得攜帶事件循環或處理Python的非同步編碼習慣時,也可以使用同步的方式調用非同步代碼。
使用@tworoutine設計流程。同步和非同步實現被替換為可以混合語法的單個實現。
感慨
使用@tworoutine的日子可能已經屈指可數了。 Python開發人員已經堅決地拒絕了這種編碼風格:
issue 22239:asyncio:嵌套事件循環(https://bugs.python.org/issue22239)
我們幾年來一直在南極和其他地方使用這種方法(在Python 2.7和Tornado < 4.5上實現)。
要完成同步@tworoutine調用,我們需要獲取一個事件循環,調度非同步調用,並阻塞直到它完成。目前在沒有修補它的情況下,沒有辦法在Python 3.7 asyncio中這樣做。調用堆棧中任何一點的非同步代碼必須只能通過非同步調用鏈接到事件循環,一直向上。
要在此處顯示的Python 3.7代碼中解決此問題,我使用了nest_asyncio補丁。它是一個簡短而有效的代碼片段,但它與Python正統相反,並且在生產環境中採用這種補丁會帶來極大的風險。
如果沒有這個補丁,我們可以在Python 3.x上升級到Tornado 4.5,但是Tornado 5.0會轉移到asyncio事件循環,這導致我們最終無法升級。
最終放棄
這裡的代碼示例已經從Python 2.7和Tornado 4.5轉移到Python 3.7和純asyncio。這是一個實驗——這不是生產代碼!
英文原文:http://threespeedlogic.com/python-tworoutines.html
譯者:xiaocai
※Django意欲改革管理機構和模式
※Python在嵌入式系統中的崛起仍在繼續
TAG:Python部落 |