時間:2017-12-19 來源:互聯網 瀏覽量:
例子1: follow.py 可以使用生成器完成 tail -f 的功能,也就是跟蹤輸出的功能。
例子2: 生成器用作程序管道(類似unix pipe)
pipeline.py
理解pipeline.py
在pipeline中,follow函數和grep函數相當於程序鏈,這樣就能鏈式處理程序。
Yield作為表達【我們開始說協程了~】:
grep.py
yield最重要的問題在於yield的值是多少。
yield的值需要使用coroutine協程這個概念 相對於僅僅生成值,函數可以動態處理傳送進去的值,而最後值通過yield返回。
協程的執行:
協程的執行和生成器的執行很相似。 當你初始化一個協程,不會返回任何東西。 協程隻能響應run和send函數。 協程的執行依賴run和send函數。 如果你缺誌同道合的朋友!缺學習Python的氛圍!缺入門資料和視頻!缺書籍的PDF!缺遇到問題沒人解答?那就加這個群:103456743 你想要一起學習的朋友?資料免費提供的學習環境?好比圖書館!不收你一分錢,隻為提供一個良好的交流平台!編程貴在多交流!
協程啟動:
所有的協程都需要調用.next( )函數。 調用的next( )函數將要執行到第一個yield表達式的位置。 在yield表達式的位置上,很容易去執行就可以。 協程使用next()啟動。
使用協程的修飾器:
由【協程啟動】中我們知道,啟動一個協程需要記得調用next( )來開始協程,而這個啟動器容易忘記使用。 使用修飾器包一層,來讓我們啟動協程。 【以後所有的協程器都會先有@coroutine
使用GeneratorExit這個異常類型
拋出一個異常:
在一個協程中,可以拋出一個異常
第三部分,管道過濾器:
叫過濾器其實並不貼切,應該叫中間人Intermediate:其兩端都是send()函數。
(協程的中間層) 典型的中間層如下:
協程和生成器的對比
不同處:生成器使用了迭代器拉取數據,協程使用send()壓入數據。
變得多分支:(上一個協程發送數據去多個下一段協程)
圖示:
使用協程,你可以發送數據 給 多個 協程過濾器/協程終了。但是請注意,協程源隻是用來傳遞數據的,過多的在協程源中傳遞數據是令人困惑並且複雜的。
一個例子
從文章中分別打印出含有’python‘ ’ply‘ ’swig‘ 關鍵字的句子。使用了一個協程隊列向所有printer協程 送出 接收到的數據。 圖示:
或者這樣Hook them up:
第三部分:協程,事件分發
事件處理
協程可以用在寫各種各樣處理事件流的組件。
介紹一個例子【這個例子會貫穿這個第三部分始終】要求做一個實時的公交車GPS位置監控。編寫程序的主要目的是處理一份文件。傳統上,使用SAX進行處理。【SAX處理可以減少內存空間的使用,但SAX事件驅動的特性會讓它笨重和低效】。
把SAX和協程組合在一起
我們可以使用協程分發SAX事件,比如:
【最終的組合】
比如,把xml改成json最後從中篩選的出固定信息. buses.py
協程的一個有趣的事情是,您可以將初始數據源推送到低級別的語言,而不需要重寫所有處理階段。比如,PPT 中69-73頁介紹的,可以通過協程和低級別的語言進行聯動,從而達成非常好的優化效果。如Expat模塊或者cxmlparse模塊。 ps: ElementTree具有快速的遞增xml句法分析
第四部分:從數據處理到並發編程
複習一下上麵學的特點:
協程有以下特點。
協程和生成器非常像。
我們可以用協程,去組合各種簡單的小組件。
我們可以使用創建進程管道,數據流圖的方法去處理數據。
你可以使用伴有複雜數據處理代碼的協程。
一個相似的主題:
我們往協程內傳送數據,向線程內傳送數據,也向進程內傳送數據。那麼,協程自然很容易和線程和分布式係統聯係起來。
基礎的並發:
我們可以通過添加一個額外的層,從而封裝協程進入線程或者子進程。這描繪了幾個基本的概念。
目標!協程+線程【沒有蛀牙。
下麵看一個線程的例子。 cothread.py
例子解析:第一部分:先新建一個隊列。然後定義一個永久循環的線程;這個線程可以將其中的元素拉出消息隊列,然後發送到目標裏麵。第二部分:接受上麵送來的元素,並通過隊列,將他們傳送進線程裏麵。其中用到了GeneratorExit ,使得線程可以正確的關閉。
Hook up:cothread.py
但是:添加線程讓這個例子慢了50%
目標!協程+子進程
我們知道,進程之間是不共享係統資源的,所以要進行兩個子進程之間的通信,我們需要通過一個文件橋接兩個協程。
程序通過sendto()和recvfrom()傳遞文件。
和環境結合的協程:
使用協程,我們可以從一個任務的執行環境中剝離出他的實現。並且,協程就是那個實現。執行環境是你選擇的線程,子進程,網絡等。
需要注意的警告 :
創建大量的協同程序,線程和進程可能是創建 不可維護 應用程序的一個好方法,並且會減慢你程序的速度。需要學習哪些是良好的使用協程的習慣。
在協程裏send()方法需要被適當的同步。
如果你對已經正在執行了的協程使用send()方法,那麼你的程序會發生崩潰。如:多個線程發送數據進入同一個協程。
同樣的不能創造循環的協程:
堆棧發送正在構建一種調用堆棧(send()函數不返回,直到目標產生)。
如果調用一個正在發送進程的協程,將會拋出一個錯誤。
send() 函數不會掛起任何一個協程的執行。
第五部分:任務一樣的協程
Task的概念
在並發編程中,通常將問題細分為“任務”。 “任務”有下麵幾個經典的特點: * 擁有獨立的控製流。 * 擁有內在的狀態。 * 可以被安排規劃/掛起/恢複。 * 可與其他的任務通信。 協程也是任務的一種。
協程是任務的一種:
下麵的部分 來告訴你協程有他自己的控製流,這裏 if 的控製就是控製流。
需要解決的問題(還在複習微嵌知識)
CPU執行的是應用程序,而不是你的操作係統,那 沒有被CPU執行的操作係統 是怎麼控製 正在運行的應用程序 中斷的呢。
中斷(interrupts)和陷阱(Traps)
操作係統隻能通過兩個機製去獲得對應用程序的控製:中斷和陷阱。 * 中斷:和硬件有關的balabala。 * 陷阱:一個軟件發出的信號。 在兩種狀況下,CPU都會掛起正在做的,然後執行OS的代碼(這個時候,OS的代碼成功插入了應用程序的執行),此時,OS來切換了程序。
中斷的底層實現(略…碼字員微嵌隻有70分��♀️)
中斷的高級表現:
* 中斷(Traps)使得OS的代碼可以實現。* 在程序運行遇到中斷(Traps)時,OS強製在CPU上停止你的程序。* 程序掛起,然後OS運行。
表現如下圖:
每次中斷(Traps)程序都會執行另一個不同的任務。
任務調度(非常簡單):
為了執行很多任務,添加一簇任務隊列。
啟示(很重要):
BB了這麼多微嵌的內容,得到的是什麼結論呢。類比任務調度,協程中yield聲明可以理解為中斷(Traps)。當一個生成器函數碰到了yield聲明,那函數將立即掛起。而執行被傳給生成器函數運行的任何代碼。如果你把yield聲明看成了一個中斷,那麼你就可以組件一個多任務執行的操作係統了。
第七部分:讓我們建一個操作係統。【起飛了,請握好扶手
目標:滿足以下條件建成一個操作係統。
1. 用純python語句。2. 不用線程。3. 不用子進程。4. 使用生成器和協程器。
我們用python去構建操作係統的一些動機:
* 尤其在存在線程鎖(GIL)的條件下,在線程間切換會變得非常重要。我要高並發!* 不阻塞和異步I/O。我要高並發!* 在實戰中可能會遇到:服務器要同時處理上千條客戶端的連接。我要高並發!* 大量的工作 致力於實現 事件驅動 或者說 響應式模型。我要組件化!* 綜上,python構建操作係統,有利於了解現在高並發,組件化的趨勢。
第一步:定義任務
定義一個任務類:任務像一個協程的殼,協程函數傳入target;任務類僅僅有一個run()函數。 pyos1.py
在foo中,yield就像中斷(Traps)一樣,每次執行run(),任務就會執行到下一個yield(一個中斷)。
第二步:構建調度者
下麵是調度者類,兩個屬性分別是Task隊列和task_id與Task類對應的map。schedule()向隊列裏麵添加Task。new()用來初始化目標函數(協程函數),將目標函數包裝在Task,進而裝入Scheduler。最後mainloop會從隊列裏麵拉出task然後執行到task的target函數的yield為止,執行完以後再把task放回隊列。這樣下一次會從下一個yield開始執行。 pyos2.py
第三步:確定任務的停止條件
如果,target函數裏麵不是死循環,那麼上麵的代碼就會出錯。所以我們對Scheduler做改進。添加一個從任務隊列中刪除的操作,和對於StopIteration的驗證。 【對scheduler做改進的原因是任務的性質:可以被安排規劃/掛起/恢複。】
第四步:添加係統調用基類。
在OS中,中斷是應用程序請求係統服務的方式。在我們的代碼中,OS是調度者(scheduler),而中斷是yield。為了請求調度者服務,任務需要帶值使用yield聲明。 pyos4.py
代碼解析: 1. 如果taskmap裏麵存在task,就從ready隊列裏麵拿任務出來,如果沒有就結束mainloop。 2. 【就是傳說中的係統調運部分】ready隊列裏麵的task被拿出來以後,執行task,返回一個result對象,並初始化這個result對象。如果隊列裏麵的task要停止迭代了(終止yield這個過程)就從隊列裏刪除這個任務。 3. 最後再通過schedule函數把執行後的task放回隊列裏麵。 4. 係統調用基類,之後所有的係統調用都要從這個基類繼承。
第4.5步:添加第一個係統調用
這個係統調用想返回任務的id。 Task的sendval屬性就像一個係統調用的返回值。當task重新運行的是後,sendval將會傳入這個係統調用。 pyos4.py
理解這段代碼的前提: (非常重要) 1. send()函數有返回值的,返回值是yield表達式右邊的值。在本段代碼中,result的返回值是yield GetTid()的GetTid的實例或者是yield後麵的。 2. 執行send(sendval)以後,sendval被傳入了yield表達式。並賦給了mytid,返回GetTid()給ruselt。
執行順序: 先創建一個調度者(Scheduler),然後在調度者裏麵添加兩個協程函數:foo(), bar(),最後觸發mainloop進行協程的調度執行。
係統調用原理: 係統調用是基於係統調用類實現的,如GetTid類,其目的是傳出自己的tid。傳出自己的tid之後,再將task放回隊列。
第五步:任務管理
上麵我們搞定了一個GetTid係統調用。我們現在搞定更多的係統調用: * 創建一個新的任務。 * 殺掉一個已經存在的任務。 * 等待一個任務結束。 這些細小的相同的操作會與線程,進程配合。
1. *創建一個新的係統調用*:通過係統調用加入一個task。
網絡服務器的搭建:
現在已經完成了: * 多任務。 * 開啟新的進程。 * 進行新任務的管理。 這些特點都非常符合一個web服務器的各種特點。下麵做一個Echo Server的嚐試。
但問題是這個網絡服務器是I / O阻塞的。整個python的解釋器需要掛起,一直到I/O操作結束。
非阻塞的I/O
先額外介紹一個叫Select的模塊。select模塊可以用來監視一組socket鏈接的活躍狀態。用法如下:
下麵實現一個非阻塞I/O的網絡服務器,所用的思想就是之前所實現的Task waiting 思想。源碼解析:__init__裏麵的是兩個字典。用來存儲阻塞的IO的任務。waitforread()和waitforwrite()將需要等待寫入和等待讀取的task放在dict裏麵。這裏的iopoll():使用select()去決定使用哪個文件描述器,並且能夠不阻塞任意一個和I/O才做有關係的任務。poll這個東西也可以放在mainloop裏麵,但是這樣會帶來線性的開銷增長。 詳情請見: Python Select 解析 - 金角大王 - 博客園
添加新的係統調用:
更多請見開頭那個連接裏麵的代碼:pyos8.py
這樣我們就完成了一個多任務處理的OS。這個OS可以並發執行,可以創建、銷毀、等待任務。任務可以進行I/O操作。並且最後我們實現了並發服務器。
第八部分:協程棧的一些問題的研究。
我們可能在使用yield的時候會遇到一些問題:
讓我們來看看這個叫蹦床的奇淫巧技。
代碼:trampoline.py
整個控製流如下:
我們看到,上圖中左側為統一的scheduler,如果我們想調用一個子線程,我們都用通過上麵的scheduler進行調度。
千萬不要一個函數裏麵包含兩個或多個以上的功能,比如函數是generator就是generator,是一個coroutine就是一個coroutin。
謝謝閱讀!
如有侵權請聯係小編刪除哦!