小米html
亂談Python併發node
說實話,我一直以爲PHP真的是最好的語言,不只養活了一大批PHP程序員,同時還爲安全人員提供了大量的就業機會。然而,使人唏噓的是,安全界不少人實際上是吃着Python的飯,操着PHP的心。此外,大量的安全研究工具也都是使用Python開發,好比我始終不習慣的mitmproxy,又或者一個循環語句400行的sqlmap、一抓一大把的爬蟲框架以及subprocess滿天飛的命令行應用包裝庫。python
幹活要吃飯,吃飯要帶碗。既然這樣,要進入互聯網安全領域,不管是小白仍是高手,多少是要了解點Python的。雖然筆者只是個安全太白,連小白都夠不上,但我Python比你專業啊。我看過一些安全人員寫的代碼,不能否認,功能是有的,代碼是渣的,這我很是理解,畢竟術業有專攻,要我去挖洞我也麻瓜,挖個坑倒能夠。程序員
其實,Python能夠談的話題不少,好比Python2仍是Python3,好比WSGI,好比編碼,好比擴展,好比JIT,好比框架和經常使用庫等等,而咱們今天要說的則是異步/併發問題,代碼運行快一點,就能有更多時間找女友了。web
衆所周知,CPython存在GIL(全局解釋鎖)問題,用來保護全局的解釋器和環境狀態變量,社區有過幾回去GIL的嘗試,都以失敗了結,由於發現即便去了GIL,性能好像提升也不是那麼明顯嘛,還搞那麼複雜。注意,這裏說的是CPython,Python語言自己是沒說要必須有GIL的,例如基於JVM的Jython。而GIL的結果就是Python多線程沒法利用多CPU,你128核又如何,我就逮着一隻羊薅羊毛了。因此,若是功能是CPU密集型的,這時候Python的進程就派上用場了,除此以外,利用C擴展也是能夠繞過GIL的,這是後話。redis
進程模型算是一種比較古老的併發模型。Python中的進程基本是對系統原生進程的包裝,好比Linux上的fork。在Python標準庫中,主要是multiprocessing包,多麼直白的名字。其中經常使用的也就是pool,queue模塊以及synchronize模塊中的一些同步原語(Lock、Condition、Semaphore、Event等)。若是須要更高級的功能,能夠考慮下managers模塊,該模塊用來管理同步進程,大體的實現原理是在內部起了一個server,進程都與這個server交互,進行變量共享...目瞪狗呆有沒有,這個模塊筆者也只用過兩三次,如需對進程進行高級管理,請移步此處。算法
另外,multiprocessing中有個dummpy子模塊,從新實現了一遍多進程模塊中的API,然而,它是多線程的,就這麼亂入,目的是方便你的代碼在多線程和多進程之間無障礙切換,很貼心有沒有,並且異常低調,低調到官方文檔就一句話,17個單詞。sql
若是你的代碼須要大量的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主要是用來作生成器的,不要告我不知道什麼叫生成器,這多尷尬。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的短板。
這算第二階段,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):
這是第三個階段。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,誰用誰知道。
除了官方的協程實現外,還有一些基於協程的框架或網絡庫,其中比較有名的有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。
360
咱們目前有Python代碼約6萬行,程序運行在Linux下。
這6萬行Python代碼被被分紅80餘個項目進行組織。每一個項目提供一個或一組完整的功能集合,每一個項目都有本身的 setup.py 文件用來將項目代碼打包成 Python 發佈包(Distribution),部分項目還有自動文檔生成,咱們使用的是 Sphinx 和 reST格式的文本。打包好的Python包被髮布到咱們本身搭建的內網的與 pypi.python.org 兼容的私有 pypi 服務器上,而文檔保存在內網的相似於 readthedocs 的服務器上。
後臺團隊的代碼主要運行咱們本身的Linux服務器集羣上,開發和部署的成本比較低,所以咱們使用比較敏捷的開發流程。流程大致上能夠分爲下面幾個步驟:
熟悉Python的朋友們可能看到這些名詞和包都很熟悉,由於咱們所使用的都是業界普遍使用的開發、測試和運維的工具。但這些工具不少都適合於開源軟件(Open Source Software)而非私有軟件(Proprietary Software),例如 distribute 與 pypi.python.org 的結合是完美無缺的,Sphinx 和 readthedocs 也是很容易結合,可是做爲一個私有軟件,咱們沒法將代碼和文檔放到 pypi 或 readthedocs 上面。爲此咱們幾乎複製了整套的基礎架構,包括 pypi 服務,readthedocs 服務等,後續我會介紹咱們如何作到這點的。但願這個系列對於其餘正在使用Python開發私有軟件的同仁能有些幫助。