技術分享 | 亂談 Python 并發

cai_xiang 8年前發布 | 8K 次閱讀 Python 并發 Python開發

亂談Python并發

說實話,我一直覺得PHP真的是最好的語言,不僅養活了一大批PHP程序員,同時還為安全人員提供了大量的就業機會。然而,令人唏噓的是,安全界很多人其實是吃著Python的飯,操著PHP的心。此外,大量的安全研究工具也都是使用Python開發,比如我始終不習慣的mitmproxy,又或者一個循環語句400行的sqlmap、一抓一大把的爬蟲框架以及subprocess滿天飛的命令行應用包裝庫。

干活要吃飯,吃飯要帶碗。既然這樣,要進入互聯網安全領域,無論是小白還是高手,多少是要了解點Python的。雖然筆者只是個安全太白,連小白都夠不上,但我Python比你專業啊。我看過一些安全人員寫的代碼,不可否認,功能是有的,代碼是渣的,這我非常理解,畢竟術業有專攻,要我去挖洞我也麻瓜,挖個坑倒可以。

其實,Python可以談的話題很多,比如Python2還是Python3,比如WSGI,比如編碼,比如擴展,比如JIT,比如框架和常用庫等等,而我們今天要說的則是異步/并發問題,代碼運行快一點,就能有更多時間找女朋友了。

進程

眾所周知,CPython存在GIL(全局解釋鎖)問題,用來保護全局的解釋器和環境狀態變量,社區有過幾次去GIL的嘗試,都以失敗告終,因為發現即使去了GIL,性能好像提高也不是那么明顯嘛,還搞那么復雜。注意,這里說的是CPython,Python語言本身是沒說要必須有GIL的,例如基于JVM的Jython。而GIL的結果就是Python多線程無法利用多CPU,你128核又如何,我就逮著一只羊薅羊毛了。所以,如果功能是CPU密集型的,這時候Python的進程就派上用場了,除此之外,利用C擴展也是可以繞過GIL的,這是后話。

進程模型算是一種比較古老的并發模型。Python中的進程基本是對系統原生進程的包裝,比如Linux上的fork。在Python標準庫中,主要是multiprocessing包,多么直白的名字。其中常用的也就是pool,queue模塊以及synchronize模塊中的一些同步原語(Lock、Condition、Semaphore、Event等)。如果需要更高級的功能,可以考慮下managers模塊,該模塊用來管理同步進程,大致的實現原理是在內部起了一個server,進程都與這個server交互,進行變量共享...目瞪狗呆有沒有,這個模塊筆者也只用過兩三次,如需對進程進行高級管理,請移步此處。

另外,multiprocessing中有個dummpy子模塊,重新實現了一遍多進程模塊中的API,然而,它是多線程的,就這么亂入,目的是方便你的代碼在多線程和多進程之間無障礙切換,很貼心有沒有,而且異常低調,低調到官方文檔就一句話,17個單詞。

如果你的代碼需要大量的CPU運算,多進程是一個比較好的選擇。對于安全領域的來說,這種場景貌似不是很多,什么?需要大量加密解密?都到自己要 實現這么高深算法的程度了,別掙扎了,用C吧,寫個擴展更好。

所以,如非必須,我是不太推薦用多進程的,容易出錯,不好控制,而且,有更省心的選擇,誰會和自己過不去呢。

線程

與進程一樣,Python中的線程也是對系統原生線程的包裝。其實現在的Linux上,線程和進程的差別不是很大,以此推知,Linux平臺下,Python中的線程和進程開銷差別也不會太大,但終歸進程是要開銷大點的,創建也會慢一點。相比于進程,我是更傾向使用線程的,尤其是IO密集型程序,能用線程解決的問題,盡量不用進程。

另外,如果要在進程之間共享數據,確實比較頭疼一點,要用到Queue、Pipe、SyncManager或者類似redis這種外部依賴,而線程之間共享數據就方便很多,畢竟大家都是一個爹生的,家里東西一起用吧。有人可能會覺得,線程能共享數據,但是也會在修改數據時互相影響,導致各種難以排查的BUG,這個問題提的好,之所以有這種問題,還不是因為代碼寫的爛,多練練就好了。如果既想要方便的共享數據,還要能隨意的隔離數據,threading.local()可以幫你,創建的變量屬于線程隔離的,線程之間互不影響,上帝的歸上帝,愷撒的歸愷撒。說到ThreadLocal變,我們熟知的Flask中每個請求上下文都是ThreadLocal的,以便請求隔離,這個是題外話。

Python中的線程主要是在threading模塊里,這個模塊是對更底層的\_thread的封裝,提供了更友好的接口。該模塊中用到比較多的也是Queue、Pool、Lock、Event等,這些就不展開了,有機會再一一細說。Python 3.2后還引入了一個比較有意思的新類,叫Barrier,顧名思義,就是設置個障礙(設置數目n),等大家都到齊了(每個線程調用下wait,直到有n個線程調用),再一起出發。Python 3.3也在進程中引入了對應的此模塊。此外,還有Timer可以用來處理各種超時情況,比如終結subprocess創建的進程。

創建多線程有兩種方式:一種是繼承threading.Thread類,然后實現run方法,在其中實現功能邏輯;另一種就是直接threading.Thread(target=xxx)的方式來實現,與進程模塊大同小異。具體使用可以參考官方文檔,這里就不贅述了。

協程

前面我們提到了,Python的線程(包括進程)其實都是對系統原生內核級線程的包裝,切換時,需要在內核與用戶態空間來來回回,開銷明顯會大很多,而且多個線程完全由系統來調度,什么時候執行哪個線程是無法預知的。相比而言,協程就輕量級很多,是對線程的一種模擬,原理與內核級線程類似,只不過切換時,上下文環境保存在用戶態的堆棧里,協程“掛起”的時候入棧,“喚醒”的時候出棧,所以其調度是可以人為控制的,這也是“協程”名字的來由,大伙協作著來,別總搶來搶去的,傷感情。

實際上,協程本身并不是真正的并發,任何時候只有一個協程在執行,只是當需要耗時操作時,比如I/O,我們就讓它掛起,執行別的協程,而不是一直干等著什么也做不了,I/O完畢了我們再切換來繼續執行,這樣可以大大提高效率,而且不用再費心費力去考慮同步問題,簡單高效。與傳統線程、進程模型相比,協程配上事件循環(告訴協程什么時候掛起,什么時候喚醒),簡直完美。Python里的協程也是后來才逐漸加入的,基本分三個階段,過程比較坎坷,與”攜程“差不多,時不時被罵幾句。

yield/send

這算是第一個階段,其實yield主要是用來做生成器的,不要告我不知道什么叫生成器,這多尷尬。Python 2.5時,yield變成了表達式(之前只是個語句),這樣就有了值,同時PEP 342引入了send,yield可以暫停函數執行,send通知函數繼續往下執行,并提供給yield值。仔細一看,好巧啊,這么像協程,于是屁顛屁顛的把生成器用來實現協程,雖然是半吊子工程,不過還不錯的樣子,總比沒有的好,自此我們也可以號稱是一門有協程的現代高級編程語言了。

可以這么說,對于不考慮yield返回值情形,我們就把它當作普通的生成器,對于考慮yield返回值的,我們就可以把它看作是協程了。

但是,生成器干協程的活,總歸不是那么專業。雖然生成器這貨能模擬協程,但是模擬終歸是模擬,不能return,不能跨堆棧,局限性很大。說到這里 ,連不起眼的Lua都不屑于和我們多說話,Go則在Goroutine(說白了還不是類協程)的道上一路狂奔,頭都不回,而Erlang輕輕撫摸了下Python的頭, 說句:孫子誒。

既然Python 2.x中的yield不爭氣,索性我們來改造下咯,于是Python 3.3(別老抱著Python 2不放了)中生成器函數華麗麗的可以return了,順帶來了 個yield from,解決了舊yield的短板。

asyncio/yield from

這算第二階段,Python 3.3引入yield from(PEP 380),Python3.4引入asyncio。其實本質上來說,asyncio是一個事件循環,干的活和libev差不多, 用來調度協程,同時使用@asyncio.coroutine來把函數打扮成協程,搭配上yield from實現基于協程的異步并發。

與yield相比,yield from進化程度明顯高了很多,不僅可以用于重構簡化生成器,進而把生成器分割成子生成器,還可以像一個雙向管道一樣,將send 的信息傳遞給內層協程,獲取內層協程yield的值,并且處理好了各種異常情況,`return (yield from xxx)`也是溜溜的。

接下來看一個yield from Future的例子,其實就是asyncio.sleep(1):

async/await

這是第三個階段。Python 3.5引入了async/await,沒錯,我們就是抄C#的,而且還抄出了具有Python特色的`async with`和`async for`。某種程度上 看,async/await是asyncio/yield from的升級版,這下好了,從語言層面得到了的支持,我們是名正言順的協程了,再也不用寄人籬下,委身于生成器 了(提褲子不認人啊,其實還不是asyncio幫襯著)。也是從這一版本開始,生成器與協程的界限要逐漸開始劃清。

對比下async/await和asyncio/yield from,如此相似,不過還是有一些區別的,比如await和yield from接受的類型,又比如函數所屬類型等,所以二 者是不能混用的:

不過話說回來,最近幾個版本的Python引入的東西真不少,概念一個一個的,感覺已經不是原來那個單純的Python了。悲劇的是用的人卻不多,周邊生 態一片貧瘠,社區那幫人都是老司機,您車開這么快,我等趕不上啊,連Python界的大神愛民(Armin Ronacher,我是這么叫的,聽著接地氣)都一臉 蒙逼,狂吐槽( http://lucumr.pocoo.org/2016/10/30/i-dont-understand-asyncio ),眼看著就要跑去搞rust了。不過,吐槽歸吐槽,這個畢竟是 趨勢,總歸是要了解的,技多不壓身,亂世出英雄,祝你好運。

題外話,搞安全么,難免寫個爬蟲、發個http請求什么的,還在用requests嗎,去試試aiohttp,誰用誰知道。

greenlet/gevent與tornado

除了官方的協程實現外,還有一些基于協程的框架或網絡庫,其中比較有名的有gevent和tornado,我個人強烈建議好好學學這兩個庫。

gevent是一個基于協程的異步網絡庫,基于libev與greenlet。鑒于Python 2中的協程比較殘疾,greenlet基本可以看作是Python 2事實上的協程實現了 。與官方的各種實現不同,greenlet底層是C實現的,盡管stackless python基本上算失敗了,但是副產品greenlet卻發揚光大,配合libev也算活的有 聲有色,API也與標準庫中的線程很類似,分分鐘上手。同時猴子補丁也能很大程度上解決大部分Python庫不支持異步的問題,這時候nodejs的同學一定 在偷笑了:Python這個渣渣。

tornado則是一個比較有名的基于協程的網絡框架,主要包含兩部分:異步網絡庫以及web框架。東西是好東西,相比twisted也挺輕量級,但是配套不完 善啊,到現在我都沒找到一個好用的MySQL驅動,之前用的Redis驅動還坑的我不要不要的。我覺得用作異步網絡庫還是相當不錯的,但是作為web框架吧 ...就得看人了,我見過很多人直接用普通的MySQLdb,還告我說tornado性能高,你在逗我嗎,用普通的MySQL驅動配合異步框架,這尼瑪當單線程在用 啊,稍有差錯IOLoop Block到死,我要是tornado我都火大。隨著Python的發展,tornado現在也已經支持asyncio以及async/await,不過我很久沒用了 ,具體如何請參考文檔。

對比gevent與tornado,本質上是相同的,只是二者走了不同的道路,gevent通過給標準庫的socket、thread、ssl、os等打patch,采用隱式的方式,無 縫的把現有的各種庫轉換為支持異步,避免了為支持異步而重寫,解決了庫的問題,性能也是嗖嗖的,隨隨隨便跑萬兒八千個patch后的線程玩一樣,然 而,我對這種隱藏細節、不可掌控的黑魔法總是有一絲顧慮;另一方的tornado則采用顯示的方式,把調度交給用戶來完成,清晰明了,結果就是自成一 套體系,沒法很好的利用現有的很多庫,還得顯示的調用IOLoop,單獨使用異常別扭,你可以試試nsq的官方Python庫,都是淚。

本文只是對Python中并發編程的一個全局性的介紹,幫助不了解這方面的同學有一個概念,方便去針對學習,若要展開細節,恐怕三天三夜也講不完, 而我的碗還沒洗,所以這次就到此為止。其實,作為一門通用膠水語言,我覺得,無論工作是哪個方向,好好學習一下Python是有必要的。知道 requests那哥們嗎,寫完requests后就從路人大胖子變成了文藝攝影小帥哥,而且還抱得美人歸,你還等什么。退一步講,萬一安全搞不好,還可以考 慮進軍目前火熱的機器學習領域。

所以,人生苦短,我用Python。

 

 

 

來自:http://mp.weixin.qq.com/s?__biz=MzI2NzI2OTExNA==&mid=2247484013&idx=1&sn=c4403efdb47bfb7f7d420859ad55debf&chksm=ea8024f8ddf7adeecb0131a67e4415a2a49129faa8f14a363d67babaa91b04399209fed7b30a#rd

 

 本文由用戶 cai_xiang 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!