標籤:patch 處理過程 先來 執行個體 gui gevent 通訊 任務調度 培養
協程
協程,又稱微線程,纖程。英文名Coroutine。
協程的概念很早就提出來了,但直到最近幾年才在某些語言(如Lua)中得到廣泛應用。
子程式,或者稱為函數,在所有語言中都是層級調用,比如A調用B,B在執行過程中又調用了C,C執行完畢返回,B執行完畢返回,最後是A執行完畢。
所以子程式調用是通過棧實現的,一個線程就是執行一個子程式。
子程式調用總是一個入口,一次返回,調用順序是明確的。而協程的調用和子程式不同。
協程看上去也是子程式,但執行過程中,在子程式內部可中斷,然後轉而執行別的子程式,在適當的時候再返回來接著執行。
線程和進程的操作是由程式觸發系統介面,最後的執行者是系統;協程的操作則是程式員。
協程可以被認為是一種使用者空間線程,與傳統的搶佔式線程相比,有2個主要的優點:
- 與線程不同,協程是自己主動讓出CPU,並交付他期望的下一個協程運行,而不是在任何時候都有可能被系統調度打斷。因此協程的使用更加清晰易懂,並且多數情況下不需要鎖機制。
- 與線程相比,協程的切換由程式控制,發生在使用者空間而非核心空間,因此切換的代價非常的小。
- 某種意義上,協程與線程的關係類似與線程與進程的關係,多個協程會在同一個線程的上下文之中運行。
協程存在的意義:對於多線程應用,CPU通過切片的方式來切換線程間的執行,線程切換時需要耗時(儲存狀態,下次繼續)。協程,則只使用一個線程,在一個線程中規定某個代碼塊執行順序。
協程的適用情境:當程式中存在大量不需要CPU的操作時(IO),適用於協程;
協程的好處:
- 無需線程環境切換的開銷
- 無需原子伺服器用戶端檔案鎖及同步的開銷
- "原子操作(atomic operation)是不需要synchronized",所謂原子操作是指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任何 context switch (切換到另一個線程)。原子操作可以是一個步驟,也可以是多個操作步驟,但是其順序是不可以被打亂,或者切割掉只執行部分。視作整體是原子性的核心。
- 方便切換控制流程,簡化編程模型
- 高並發+高擴充性+低成本:一個CPU支援上萬的協程都不是問題。所以很適合用於高並發處理。
缺點:
- 無法利用多核資源:協程的本質是個單線程,它不能同時將 單個CPU 的多個核用上,協程需要和進程配合才能運行在多CPU上.當然我們日常所編寫的絕大部分應用都沒有這個必要,除非是cpu密集型應用。
- 進行阻塞(Blocking)操作(如IO時)會阻塞掉整個程式
進程、線程和協程的區別
進程:
進程之間不共用任何狀態,進程的調度由作業系統完成,每個進程都有自己獨立的記憶體空間,進程間通訊主要是通過訊號傳遞的方式來實現的,實現方式有多種,訊號量、管道、事件等,任何一種方式的通訊效率都需要過核心,導致通訊效率比較低。由於是獨立的記憶體空間,環境切換的時候需要儲存先調用棧的資訊、cpu各寄存器的資訊、虛擬記憶體、以及開啟的相關控制代碼等資訊,所以導致上下文進程間切換開銷很大,通訊麻煩。
線程:
線程之間共用變數,解決了通訊麻煩的問題,但是對於變數的訪問需要鎖,線程的調度主要也是有作業系統完成,一個進程可以擁有多個線程,但是其中每個線程會共用父進程像作業系統申請資源,這個包括虛擬記憶體、檔案等,由於是共用資源,所以建立線程所需要的系統資源佔用比進程小很多,相應的可建立的線程數量也變得相對多很多。線程時間的通訊除了可以使用進程之間通訊的方式以外還可以通過共用記憶體的方式進行通訊,所以這個速度比通過核心要快很多。另外在調度方面也是由於記憶體是共用的,所以環境切換的時候需要儲存的東西就像對少一些,這樣一來內容相關的切換也變得高效。
協程:
協程的調度完全由使用者控制,一個線程可以有多個協程,使用者建立了幾個線程,然後每個線程都是迴圈按照指定的任務清單順序完成不同的任務,當任務被堵塞的時候執行下一個任務,當恢複的時候再回來執行這個任務,任務之間的切換隻需要儲存每個任務的上下文內容,就像直接操作棧一樣的,這樣就完全沒有核心切換的開銷,可以不加鎖的訪問全域變數,所以內容相關的切換非常快;另外協程還需要保證是非堵塞的且沒有相互依賴,協程基本上不能同步通訊,多採用一步的訊息通訊,效率比較高。
網路編程模型
我們首先來簡單回顧一下一些常用的網路編程模型。網路編程模型可以大體的分為同步模型和非同步模型兩類。
同步模型使用阻塞IO模式,在阻塞IO模式下調用read等IO函數時會阻塞線程直到IO完成或失敗。 同步模型的典型代表是thread_per_connection模型,每當阻塞在主線程上的accept調用返回時則建立一個新的線程去服務於新的socket的讀/寫。這種模型的優點是程式邏輯簡潔,符合人的思維;缺點是延展性收到線程數的限制,當串連越來越多時,線程也越來越多,頻繁的線程切換會嚴重拖累效能,同時不得不處理多線程同步的問題。
非同步模型一般使用非阻塞IO模式,並配合epoll/select/poll等多工機制。在非阻塞模式下調用read,如果沒有資料可讀則立即返回,並通知使用者沒有可讀(EAGAIN/EWOULDBLOCK),而非阻塞當前線程。非同步模型可以使一個線程同時服務於多個IO對象。 非同步模型的典型代表是reactor模型。在reactor模型中,我們將所有要處理的IO事件註冊到一個中心的IO多工器中(一般為epoll/select/poll),同時主線程阻塞在多工器上。一旦有IO事件到來或者就緒,多工器返回並將對應的IO事件分發到對應的處理器(即回呼函數)中,最後處理器調用read/write函數來進行IO操作。
非同步模型的特點是效能和延展性比同步模型要好很多,但是其結構複雜,不易於編寫和維護。在非同步模型中,IO之前的代碼(IO任務的提交者)和IO之後的處理代碼(回呼函數)是割裂開來的。
協程與網路編程
協程的出現出現為克服同步模型和非同步模型的缺點,並結合他們的優點提供了可能: 現在假設我們有3個協程A,B,C分別要進行數次IO操作。這3個協程運行在同一個調度器或者說線程的上下文中,並依次使用CPU。調度器在其內部維護了一個多工器(epoll/select/poll)。 協程A首先運行,當它執行到一個IO操作,但該IO操作並沒有立即就緒時,A將該IO事件註冊到調度器中,並主動放棄CPU。這時調度器將B切換到CPU上開始執行,同樣,當它碰到一個IO操作的時候將IO事件註冊到調度器中,並主動放棄CPU。調度器將C切換到cpu上開始執行。當所有協程都被“阻塞”後,調度器檢查註冊的IO事件是否發生或就緒。假設此時協程B註冊的IO時間已經就緒,調度器將恢複B的執行,B將從上次放棄CPU的地方接著向下運行。A和C同理。 這樣,對於每一個協程來說,它是同步的模型;但是對於整個應用程式來說,它是非同步模型。
編程範式
編程範式(Programming Paradigm)是某種程式設計語言典型的編程風格或者說是編程方式。隨著編程方法學和軟體工程研究的深入,特別是OO思想的普及,範式(Paradigm)以及編程範式等術語漸漸出現在人們面前。物件導向編程(OOP)常常被譽為是一種革命性的思想,正因為它不同於其他的各種編程範式。編程範式也許是學習任何一門程式設計語言時要理解的最重要的術語。
托馬斯.庫恩提出“科學的革命”的範式論之後,Robert Floyd在1979年圖靈獎的頒獎演說中使用了編程範式一詞。編程範式一般包括三個方面,以OOP為例:
- 學科的邏輯體系——規則範式:如類/對象、繼承、動態綁定、方法改寫、對象替換等等機制。
- 心理認知因素——心理範式:按照物件導向編程之父Alan Kay的觀點,“計算就是類比”。OO範式極其重視隱喻(metaphor)的價值,通過擬人化,按照自然的方式類比自然。
- 自然觀/世界觀——觀念範式:強調程式的組織技術,視程式為鬆散耦合的對象/類的集合,以繼承機制將類組織成一個階層,把程式運行視為相互服務的對象們之間的對話。
簡單的說,編程範式是程式員看待程式應該具有的觀點。
為了進一步加深對編程範式的認識,這裡介紹幾種最常見的編程範式。
需要再次提醒注意的是:編程範式是程式設計語言的一種分類方式,它並不針對某種程式設計語言。就程式設計語言而言,一種程式設計語言也可以適用多種編程範式。
過程化(命令式)編程
過程化編程,也被稱為命令式編程,應該是最原始的、也是我們最熟悉的一種傳統的編程方式。從本質上講,它是“馮.諾伊曼機“運行機制的抽象,它的編程思維方式源於電腦指令的順序排列。
(也就是說:過程化語言類比的是電腦機器的系統結構,而並不是基於語言的使用者的個人能力和傾向。這一點我們應該都很清楚,比如:我們最早曾經使用過的單片機的組合語言。)
過程化編程的步驟是:
首先,我們必須將待解問題的解決方案抽象為一系列概念化的步驟。然後通過編程的方式將這些步驟轉化為程式指令集(演算法),而這些指令按照一定的順序排列,用來說明如何執行一個任務或解決一個問題。這就意味著,程式員必須要知道程式要完成什麼,並且告訴電腦如何來進行所需的計算工作,包括每個細節操作。簡言之,就是將電腦看作一個善始善終服從命令的裝置。
所以在過程化編程中,把待解問題正常化、抽象為某種演算法是解決問題的關鍵步驟。其次,才是編寫具體演算法和完成相應的演算法實現問題的正確解決。當然,程式員對待解問題的抽象能力也是非常重要的因素,但這本身已經與程式設計語言無關了。
程式流程圖是過程化語言進行程式編寫的有效輔助手段。
儘管現存的電腦程式設計語言很多,但是人們把所有支援過程化編程範式的程式設計語言都被歸納為過程化程式設計語言。例如機器語言、組合語言、BASIC、COBOL、C 、FORTRAN、語言等等許多第三代程式設計語言都被歸納為過程化語言。
過程化語言特別適合解決線性(或者說按部就班)的演算法問題。它強調“自上而下(自頂向下)”“精益求精”的設計方式。這種方式非常類似我們的工作和生活,因為我們的日常活動都是按部就班的順序進行的。
過程化語言趨向於開發運行較快且對系統資源使用率較高的程式。過程化語言非常的靈活並強大,同時有許多經典應用範例,這使得程式員可以用它來解決多種問題。
過程化語言的不足之處就是它不適合某些種類問題的解決,例如那些非結構化的具有複雜演算法的問題。問題出現在,過程化語言必須對一個演算法加以詳盡的說明,並且其中還要包括執行這些指令或語句的順序。實際上,給那些非結構化的具有複雜演算法的問題給出詳盡的演算法是極其困難的。
廣泛引起爭議和討論的地方是:無條件分支,或goto語句,它是大多數過程式程式設計語言的組成部分,反對者聲稱:goto語句可能被無限地濫用;它給程式設計提供了製造混 亂的機會。目前達成的共識是將它保留在大多數語言中,對於它所具有的危險性,應該通過程式設計的規定將其最小化。
事件驅動編程
其實,基於事件驅動的程式設計在圖形化使用者介面(GUI)出現很久前就已經被應用於程式設計中,可是只有當圖形化使用者介面廣泛流行時,它才逐漸形演變為一種廣泛使用的程式設計模式。
在過程式的程式設計中,代碼本身就給出了程式執行的順序,儘管執行順序可能會受到程式輸入資料的影響。
在事件驅動的程式設計中,程式中的許多部分可能在完全不可預料的時刻被執行。往往這些程式的執行是由使用者與正在執行的程式的互動激發所致。
- 事件。就是通知某個特定的事情已經發生(事件發生具有隨機性)。
- 事件與輪詢。輪詢的行為是不斷地觀察和判斷,是一種無休止的行為方式。而事件是靜靜地等待事情的發生。事實上,在Windows出現之前,採用滑鼠輸入字元模式的PC應用程式必須進行串列輪詢,並以這種方式來查詢和響應不同的使用者操做。
- 事件處理器。是對事件做出響應時所執行的一段程式碼。事件處理器使得程式能夠對於使用者的行為做出反映。
事件驅動常常用於使用者與程式的互動,通過圖形使用者介面(滑鼠、鍵盤、觸摸板)進行互動互動。當然,也可以用於異常的處理和響應使用者自訂的事件等等。
事件的異常處理比使用者互動更複雜。
事件驅動不僅僅局限在GUI編程應用。但是實現事件驅動我們還需要考慮更多的實際問題,如:事件定義、事件觸發、事件轉化、事件彙總、事件排隊、事件指派、事件處理、事 件連帶等等。
其實,到目前為止,我們還沒有找到有關純事件驅動編程的語言和類似的開發環境。所有關於事件驅動的資料都是基於GUI事件的。
屬於事件驅動的程式設計語言有:VB、C#、Java(Java Swing的GUI)等。它們所涉及的事件絕大多數都是GUI事件。
物件導向編程
過程化範式要求程式員用按部就班的演算法看待每個問題。很顯然,並不是每個問題都適合這種過程化的思維方式。這也就導致了其它程式設計範式出現,包括我們現在介紹的物件導向的程式設計範式。
物件導向的程式設計模式已經出現二十多年,經過這些年的發展,它的設計思想和設計模式已經穩定的進入程式設計語言的主流。來自TIOBE Programming Community2010年11月份程式設計語言排名的前三名Java、C、C++中,Java和C++都是物件導向的程式設計語言。
物件導向的程式設計包括了三個基本概念:封裝性、繼承性、多態性。物件導向的程式語言通過類、方法、對象和訊息傳遞,來支援物件導向的程式設計範式。
1. 對象
世間萬事萬物都是對象。
物件導向的程式設計的抽象機制是將待解問題抽象為物件導向的程式中的對象。利用封裝使每個對象都擁有個體的身份。程式便是成堆的對象,彼此通過訊息的傳遞,請求其它對象 進行工作。
2. 類
每個對象都是其類中的一個實體。
物以類聚——就是說明:類是相似對象的集合。類中的對象可以接受相同的訊息。換句話說:類包含和描述了“具有共同特性(資料元素)和共同行為(功能)”的一組對象。
比如:蘋果、梨、橘子等等對象都屬於水果類。
3. 封裝
封裝(有時也被稱為資訊隱藏)就是把資料和行為結合在一個包中,並對對象的使用者隱藏資料的實現過程。資訊隱藏是物件導向編程的基本原則,而封裝是實現這一原則的一種方 式。
封裝使對象呈現出“黑盒子”特性,這是對象再利用和實現可靠性的關鍵步驟。
4. 介面
每個對象都有介面。介面不是類,而是對符合介面需求的類所作的一套規範。介面說明類應該做什麼但不指定如何作的方法。一個類可以有一個或多個介面。
5. 方法
方法決定了某個對象究竟能夠接受什麼樣的訊息。物件導向的設計有時也會簡單地歸納為“將訊息發送給對象”。
6. 繼承
繼承的思想就是允許在已存在類的基礎上構建新的類。一個子類能夠繼承父類的所有成員,包括屬性和方法。
繼承的主要作用:通過實現繼承完成代碼重用;通過介面繼承完成代碼被重用。繼承是一種規範的技巧,而不是一種實現的技巧。
7. 多態
多態提供了“介面與實現分離”。多態不但能改善程式的組織架構及可讀性,更利於開發出“可擴充”的程式。
繼承是多態的基礎。多態是繼承的目的。
合理的運用基於類繼承的多態、基於介面繼承的多態和基於模版的多態,能增強程式的簡潔性、靈活性、可維護性、可重用性和可擴充性。
物件導向技術一方面借鑒了哲學、心理學、生物學的思考方式,另一方面,它是建立在其他編程技術之上的,是以前的編程思想的自然產物。
如果說結構化軟體設計是將函數式編程技術應用到命令式語言中進行程式設計,物件導向編程不過是將函數式模型應用到命令式程式中的另一途徑,此時,模組進步為對象,過程龜縮到class的成員方法中。OOP的很多技術——抽象資料類型、資訊隱藏、介面與實現分離、對象產生功能、訊息傳遞機制等等,很多東西就是結構化軟體設計所擁有的、或者在其他程式設計語言中單獨出現。但只有在物件導向語言中,他們才共同出現,以一種獨特的合作方式互相協作、互相補充。
編程範式 = 語感
知識的學習有幾種方式:一種靠記憶,一種靠練習,一種靠培養。就拿英語學習來說吧,學單詞,單靠記憶即可;學句型、文法,光記憶是不夠的,須要勤加練習方可熟能生巧;而要講出地道的英語,光記憶和練習是遠遠不夠的。從小學到大學,甚至博士畢業,除了英語類專業的學生外,大多數人英語練了一二十年,水平如何?不客氣但很客觀地說:一個字,爛。
原因只有一個,那就是國內的英語教學方式嚴重失策。教學總是圍繞單詞、片語、句型、文法轉,缺乏對語感的重視和培養,導致學生只會‘中式英語’。同樣道理,一個慣用C語言編程的人也許很快就能寫一些C++程式,但如果他只注重C++的文法而不注重培養OOP 的語感,那麼寫出的程式一定是‘C 式C++’。與其如此,倒不如直接用C 呢。”
一句話:學習編程範式能增強程式設計語言的語感。
語感是一個人對語言的敏銳感知力,反映了他在語言方面的整體上的直覺把握能力。語感強者,能聽弦外之音,能說雙關之語,能讀雋永之作,能寫曉暢之文。這是一種綜合的素質和修養,其重要性是不言而喻的。那麼如何培養語感呢?普通的學習和訓練固不可少,但如果忽視語言背後的文化背景和思維方式,終究只是緣木求魚。編程範式正體現了編程的思維方式,因而是培養程式設計語言的語感的關鍵。
語感有了,那些設計模式、架構,甚至架構,等看似神秘高深的東西,也會自然而然地來了。
使用yield實現協程操作例子
import timeimport queuedef consumer(name): print("--->starting eating baozi...") while True: new_baozi = yield print("[%s] is eating baozi %s" % (name,new_baozi)) #time.sleep(1) def producer(): r = con.__next__() r = con2.__next__() n = 0 while n < 5: n +=1 con.send(n) con2.send(n) print("\033[32;1m[producer]\033[0m is making baozi %s" %n ) if __name__ == ‘__main__‘: con = consumer("c1") con2 = consumer("c2") p = producer()
符合什麼條件就能稱之為協程:
- 必須在只有一個單線程裡實現並發
- 修改共用資料不需加鎖
- 使用者程式裡自己儲存多個控制流程的上下文棧
- 一個協程遇到IO操作自動切換到其它協程
基於上面這4點定義,我們剛才用yield實現的程並不能算是合格的線程.
greelet指的是使用一個任務調度器和一些產生器或者協程實現協作式使用者空間多線程的一種偽並發機制,即所謂的微線程。
greelet機制的主要思想是:產生器函數或者協程函數中的yield語句掛起函數的執行,直到稍後使用next()或send()操作進行恢複為止。可以使用一個調度器迴圈在一組產生器函數之間協作多個任務。
網路架構的幾種基本的網路I/O模型:
阻塞式單線程:這是最基本的I/O模型,只有在處理完一個請求之後才會處理下一個請求。它的缺點是效能差,如果有請求阻塞住,會讓服務無法繼續接受請求。但是這種模型編寫代碼相對簡單,在應對訪問量不大的情況時是非常適合的。
阻塞式多線程:針對於單線程接受請求量有限的缺點,一個很自然的想法就是給每一個請求開一個線程去處理。這樣做的好處是能夠接受更多的請求,缺點是線上程產生到一定數量之後,進程之間需要大量進行切換內容相關的操作,會佔用CPU大量的時間,不過這樣處理的話編寫代碼的難道稍高於單進程的情況。
非阻塞式事件驅動:為瞭解決多線程的問題,有一種做法是利用一個迴圈來檢查是否有網路IO的事件發生,以便決定如何來進行處理(reactor設計模式)。這樣的做的好處是進一步降低了CPU的資源消耗。缺點是這樣做會讓程式難以編寫,因為請求接受後的處理過程由reactor來決定,使得程式的執行流程難以把握。當接受到一個請求後如果涉及到阻塞的操作,這個請求的處理就會停下來去接受另一個請求,程式執行的流程不會像線性程式那樣直觀。twisted架構就是應用這種IO模型的典型例子。
非阻塞式Coroutine(協程):這個模式是為瞭解決事件驅動模型執行流程不直觀的問題,它在本質上也是事件驅動的,加入了Coroutine的概念。
與線程/進程的區別
線程是搶佔式的調度,多個線程並存執行,搶佔共同的系統資源;而微線程是協同式的調度。
其實greenlet不是一種真正的並發機制,而是在同一線程內,在不同函數的執行代碼塊之間切換,實施“你運行一會、我運行一會”,並且在進行切換時必須指定何時切換以及切換到哪。greenlet的介面是比較簡單易用的,但是使用greenlet時的思考方式與其他並發方案存在一定區別:
1. 線程/進程模型在大邏輯上通常從並發角度開始考慮,把能夠平行處理的並且值得平行處理的任務分離出來,在不同的線程/進程下運行,然後考慮分離過程可能造成哪些互斥、衝突問題,將互斥的資源加鎖保護來保證並發處理的正確性。
2. greenlet則是要求從避免阻塞的角度來進行開發,當出現阻塞時,就顯式切換到另一段沒有被阻塞的程式碼片段執行,直到原先的阻塞狀況消失以後,再人工切換回原來的程式碼片段繼續處理。因此,greenlet本質是一種合理安排了的 串列 。
3. greenlet本質是串列,因此在沒有進行顯式切換時,代碼的其他部分是無法被執行到的,如果要避免代碼長時間佔用運算資源造成程式假死,那麼還是要將greenlet與線程/進程機制結合使用(每個線程、進程下都可以建立多個greenlet,但是跨線程/進程時greenlet之間無法切換或通訊)。
使用
一個 “greenlet” 是一個很小的獨立微線程。可以把它想像成一個堆疊框架,棧底是初始調用,而棧頂是當前greenlet的暫停位置。你使用greenlet建立一堆這樣的堆棧,然後在他們之間跳轉執行。跳轉不是絕對的:一個greenlet必須選擇跳轉到選擇好的另一個greenlet,這會讓前一個掛起,而後一個恢複。兩 個greenlet之間的跳轉稱為 切換(switch) 。
當你建立一個greenlet,它得到一個初始化過的空堆棧;當你第一次切換到它,他會啟動指定的函數,然後切換跳出greenlet。當最終棧底 函數結束時,greenlet的堆棧又編程空的了,而greenlet也就死掉了。greenlet也會因為一個未捕捉的異常死掉。
樣本:來自官方文檔樣本
from greenlet import greenletdef test1(): print 12 gr2.switch() print 34def test2(): print 56 gr1.switch() print 78gr1 = greenlet(test1)gr2 = greenlet(test2)gr1.switch()
最後一行跳轉到 test1() ,它列印12,然後跳轉到 test2() ,列印56,然後跳回 test1() ,列印34,然後 test1() 就結束,gr1死掉。這時執行會回到原來的 gr1.switch() 調用。注意,78是不會被列印的,因為gr1已死,不會再切換。
基於greenlet的架構
eventlet
eventlet 是基於 greenlet 實現的面向網路應用的並發處理架構,提供“線程”池、隊列等與其他 Python 線程、進程模型非常相似的 api,並且提供了對 Python 發行版內建庫及其他模組的超輕量並發適應性調整方法,比直接使用 greenlet 要方便得多。
其基本原理是調整 Python 的 socket 調用,當發生阻塞時則切換到其他 greenlet 執行,這樣來保證資源的有效利用。需要注意的是:
eventlet 提供的函數只能對 Python 代碼中的 socket 調用進行處理,而不能對模組的 C 語言部分的 socket 調用進行修改。對後者這類別模組,仍然需要把調用模組的代碼封裝在 Python 標準線程調用中,之後利用 eventlet 提供的適配器實現 eventlet 與標準線程之間的協作。
雖然 eventlet 把 api 封裝成了非常類似標準線程庫的形式,但兩者的實際並發執行流程仍然有明顯區別。在沒有出現 I/O 阻塞時,除非顯式聲明,否則當前正在執行的 eventlet 永遠不會把 cpu 交給其他的 eventlet,而標準線程則是無論是否出現阻塞,總是由所有線程一起爭奪運行資源。所有 eventlet 對 I/O 阻塞無關的大運算量耗時操作基本沒有什麼協助。
gevent
gevent是一個基於協程(coroutine)的Python網路函數庫,通過使用greenlet提供了一個在libev事件迴圈頂部的進階別並發API。
主要特性有以下幾點:
基於libev的快速事件迴圈,Linux上面的是epoll機制
基於greenlet的輕量級執行單元
API複用了Python標準庫裡的內容
支援SSL的協作式sockets
可通過線程池或c-ares實現DNS查詢
通過monkey patching功能來使得第三方模組變成協作式
關於Linux的epoll機制:
epoll是Linux核心為處理大批量檔案描述符而作了改進的poll,是Linux下多工IO介面select/poll的增強版本,它能顯著提高程式在大量並發串連中只有少量活躍的情況下的系統CPU利用率。epoll的優點:
支援一個進程開啟大數目的socket描述符。select的一個進程所開啟的FD由FD_SETSIZE的設定來限定,而epoll沒有這個限制,它所支援的FD上限是最大可開啟檔案的數目,遠大於2048。
IO效率不隨FD數目增加而線性下降:由於epoll只會對“活躍”的socket進行操作,於是,只有"活躍"的socket才會主動去調用 callback函數,其他idle狀態的socket則不會。
使用mmap加速核心與使用者空間的訊息傳遞。epoll是通過核心於使用者空間mmap同一塊記憶體實現的。
核心微調。
libev機制
提供了指定檔案描述符事件發生時調用回呼函數的機制。libev是一個事件迴圈器:向libev註冊感興趣的事件,比如socket可讀事件,libev會對所註冊的事件的源進行管理,並在事件發生時觸發相應的程式。
官方文檔中的樣本:
>>> import gevent>>> from gevent import socket>>> urls = [‘www.google.com.hk‘,‘www.example.com‘, ‘www.python.org‘ ]>>> jobs = [gevent.spawn(socket.gethostbyname, url) for url in urls]>>> gevent.joinall(jobs, timeout=2)>>> [job.value for job in jobs][‘74.125.128.199‘, ‘208.77.188.166‘, ‘82.94.164.162‘]
註解:gevent.spawn()方法spawn一些jobs,然後通過gevent.joinall將jobs加入到微線程執行隊列中等待其完成,設定逾時為2秒。執行後的結果通過檢查gevent.Greenlet.value值來收集。gevent.socket.gethostbyname()函數與標準的socket.gethotbyname()有相同的介面,但它不會阻塞整個解譯器,因此會使得其他的greenlets跟隨著無阻的請求而執行。
Monket patching
Python的運行環境允許我們在運行時修改大部分的對象,包括模組、類甚至函數。雖然這樣做會產生“隱式的副作用”,而且出現問題很難調試,但在需要修改Python本身的基礎行為時,Monkey patching就派上用場了。Monkey patching能夠使得gevent修改標準庫裡面大部分的阻塞式系統調用,包括socket,ssl,threading和select等模組,而變成協作式運行。
>>> from gevent import monkey ;
>>> monkey . patch_socket ()
>>> import urllib2
通過monkey.patch_socket()方法,urllib2模組可以使用在多微線程環境,達到與gevent共同工作的目的。
事件迴圈
不像其他網路程式庫,gevent和eventlet類似, 在一個greenlet中隱式開始事件迴圈。沒有必須調用run()或dispatch()的反應器(reactor),在twisted中是有 reactor的。當gevent的API函數想阻塞時,它獲得Hub執行個體(執行時間迴圈的greenlet),並切換過去。如果沒有集線器執行個體則會動態 建立。
libev提供的事件迴圈預設使用系統最快輪詢機制,設定LIBEV_FLAGS環境變數可指定輪詢機制。LIBEV_FLAGS=1為select, LIBEV_FLAGS = 2為poll, LIBEV_FLAGS = 4為epoll,LIBEV_FLAGS = 8為kqueue。
Libev的API位於gevent.core下。注意libev API的回調在Hub的greenlet運行,因此使用同步greenlet的API。可以使用spawn()和Event.set()等非同步API。
python之協程與IO操作