Python中的協程

Coroutine in Python

引言: 本文出自David Beazley 的關于協程的PPT,現在筆者將他翻譯過來。并整理成文。感謝在協程方面的專家David Beazley, 能給我們這么深入的協程上面的講座。也希望本文能給更多pythoner普及yield的更多用法,使python的這個特性能夠更加多的活躍在大家的代碼中。

例子:

http://www.dabeaz.com/coroutines/

問題:

1. 什么是協程
2. 協程怎么用
3. 要注意什么
4. 用他們好么

第一部分:生成器和協程的介紹

生成器(Generator)的本質和特點

生成器 是 可以生成一定序列的 函數
函數可以調用next()方法。

生成器的例子:

  • 例子1: follow.py
    可以使用生成器完成 tail -f 的功能,也就是跟蹤輸出的功能。
import time

def follow(thefile):
    thefile.seek(0,2)      # Go to the end of the file
    while True:
        line = thefile.readline()
      if not line:
        time.sleep(0.1)    # Sleep briefly
          continue
      yield line
  • 例子2: 生成器用作程序管道(類似unix pipe)
ps:unix pipe 
       A pipeline is a sequence of processes chained together by their standard streams 

標注:unix管道
        一個uinx管道是由標準流鏈接在一起的一系列流程.

pipeline.py

def grep(pattern,lines):
    for line in lines:
        if pattern in line:
             yield line

if __name__ == '__main__':
    from follow import follow

    # Set up a processing pipe : tail -f | grep python
    logfile  = open("access-log")
    loglines = follow(logfile)
    pylines  = grep("python",loglines)

    # Pull results out of the processing pipeline
    for line in pylines:
        print line,

理解pipeline.py
在pipeline中,follow函數和grep函數相當于程序鏈,這樣就能鏈式處理程序。

Yield作為表達【我們開始說協程了~】:

grep.py

def grep(pattern):
    print "Looking for %s" % pattern

    print "give a value in the coroutines"
    while True:
        line = (yield)
        if pattern in line:
            print line
# Example use
if __name__ == '__main__':
    g = grep("python")
    g.next()
    g.send("Yeah, but no, but yeah, but no")
    g.send("A series of tubes")
    g.send("python generators rock!")

yield最重要的問題在于yield的值是多少。

yield的值需要使用coroutine協程這個概念
相對于僅僅生成值,函數可以動態處理傳送進去的值,而最后值通過yield返回。

協程的執行:

協程的執行和生成器的執行很相似。
當你初始化一個協程,不會返回任何東西。
協程只能響應run和send函數。
協程的執行依賴run和send函數。

協程啟動:

所有的協程都需要調用.next( )函數。
調用的next( )函數將要執行到第一個yield表達式的位置。
在yield表達式的位置上,很容易去執行就可以。
協程使用next()啟動。

使用協程的修飾器:

由【協程啟動】中我們知道,啟動一個協程需要記得調用next( )來開始協程,而這個啟動器容易忘記使用。
使用修飾器包一層,來讓我們啟動協程。
【以后所有的協程器都會先有@coroutine


def coroutine(func):
        def start(*args, **kwargs):
            cr = func(*args, **kwargs)
          cr.next()
          return cr
    return start

@coroutine
def grep(pattern):
    ...

關閉一個協程:

使用close()來關閉。

使用except捕獲協程的關閉close():

grepclose.py

@coroutine
def grep(pattern):
        print "Looking for %s" % pattern
        try:
            while True:
            line = (yield)
              if pattern in line:
                print line,
        except GeneratorExit:
            print "Going away.  Goodbye"

使用GeneratorExit這個異常類型

拋出一個異常:

在一個協程中,可以拋出一個異常

    g.throw(RuntimeError,"You're hosed")

Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 4, in grep
    RuntimeError: You're hosed

異常起源于yield表達式
可以用常規方法去抓取

一些小tips

* 盡管有點相似,但是生成器和協程是*兩個完全不同的概念*。
* 生成器用來產生序列。
* 協程用來處理序列。
* 很容易產生一些誤解。因為協程有的時候用來對進程里面的用來產生迭代對象的生成器作微調。

生成器不能夠同時生成值和接受值

* 不能往generator里面send東西。
* 協程和迭代器的概念沒有關系
* 雖然有一種用法,確實是在一個協程里面生成一些值,但是并不和迭代器有關系。

第二部分:協程,管道,數據流

進程管道:如下圖所示,一連串進程串起來像管道一樣。

image

協程可以用來作為進程管道。
你僅僅需要把協程連接在一起,然后通過send()操作傳遞數據。
整個進程管道由三部分組成:

第一部分,管道源/協程源:

進程管道需要一個初始的源(一個生產者)。
這個初始的源驅動整個管道。
管道源不是協程。

第二部分,管道終止/協程終止:

管道必須有個終止點。
管道終止/協程終止是進程管道的終止點。

例子:以實現tail -f 功能為例子

from coroutine import coroutine

# A data source.  This is not a coroutine, but it sends
# data into one (target)
import time
def follow(thefile, target):
    thefile.seek(0,2)      # Go to the end of the file
    while True:
         line = thefile.readline()
         if not line:
             time.sleep(0.1)    # Sleep briefly
             continue
         target.send(line)

# A sink.  A coroutine that receives data
@coroutine
def printer():
    while True:
         line = (yield)
         print line,

# Example use
if __name__ == '__main__':
    f = open("access-log")
    follow(f,printer())

分析:第一個follow函數是協程源,第二個printer函數是協程終止。協程源不是一個協程,但是需要傳入一個已經初始化完畢的協程。在協程源當中,調用send()。


image

第三部分,管道過濾器:

叫過濾器其實并不貼切,應該叫中間人Intermediate:其兩端都是send()函數。


image

(協程的中間層)
典型的中間層如下:


    @coroutine
    def filter(target):  # 這個target是傳遞參數的對象
        while True:
            item = (yield)  # 這里用來接收上一個send()傳入的value
            # Transform/filter item
            # processing items
            # Send it along to the next stage
            target.send(item)  # 像target傳遞參數    

分析可知,中間層需要接受上一個coroutine,也需要往下一個coroutine里面傳遞值。

一個管道過濾器的例子
從文章中找出具有“python”關鍵字的句子打印。
grep.py:

    @coroutine
    def grep(pattern, target):  # 這個target用來接收參數
        while True:
            line = (yield)  # 這里用來接收上一個send()傳入的value
            # Transform/filter item
            # processing items
            if pattern in line:
                target.send(line)
            # Send it along to the next stage

Hook it up with follow and printer:

    f = open("access-log")
  follow(f, grep('python', printer())) 

grep 從中間傳入follow,然后printer傳入grep。

image

協程和生成器的對比

image

不同處:生成器使用了迭代器拉取數據,協程使用send()壓入數據。

變得多分支:(上一個協程發送數據去多個下一段協程)

圖示:

image

使用協程,你可以發送數據 給 多個 協程過濾器/協程終了。但是請注意,協程源只是用來傳遞數據的,過多的在協程源中傳遞數據是令人困惑并且復雜的。

一個例子

?????@coroutine
def broadcast(targets):
    while True:
        item = (yield)
        for target in targets:
            target.send(item)

Hook it Up!

if __name__ == '__main__':
    f = open("access-log")
    follow(f,
       broadcast([grep('python',printer()),
                  grep('ply',printer()),
                  grep('swig',printer())])
           )

從文章中分別打印出含有’python‘ ’ply‘ ’swig‘ 關鍵字的句子。使用了一個協程隊列向所有printer協程 送出 接收到的數據。
圖示:


image

或者這樣Hook them up:

if __name__ == '__main__':
    f = open("access-log")
    p = printer()
    follow(f,
       broadcast([grep('python',p),
                  grep('ply',p),
                  grep('swig',p)])
           )

圖示:


image

為什么我們用協程

  • 協程相較于迭代器,存在更加強大的數據路由(就像上圖的數據流向)的可能。
  • 協程可以將一系列簡單的數據處理組件,整合到管道,分支,合并等復雜的布置當中。
  • 但有些限制…【后文會說】
    相對于對象的優勢
  • 從概念上簡單一點:協程就是一個函數,對象要構建整個對象。
  • 從代碼執行角度上來說,協程相對要快一些。

第三部分:協程,事件分發

事件處理

協程可以用在寫各種各樣處理事件流的組件。

介紹一個例子【這個例子會貫穿這個第三部分始終】要求做一個實時的公交車GPS位置監控。編寫程序的主要目的是處理一份文件。傳統上,使用SAX進行處理。【SAX處理可以減少內存空間的使用,但SAX事件驅動的特性會讓它笨重和低效】。

把SAX和協程組合在一起

我們可以使用協程分發SAX事件,比如:


import xml.sax

class EventHandler(xml.sax.ContentHandler):
    def __init__(self,target):
        self.target = target
    def startElement(self,name,attrs):
        self.target.send(('start',(name,attrs._attrs)))
    def characters(self,text):
        self.target.send(('text',text))
    def endElement(self,name):
        self.target.send(('end',name))

# example use
if __name__ == '__main__':
    from coroutine import *

    @coroutine
    def printer():
        while True:
            event = (yield)
            print event
    xml.sax.parse("allroutes.xml",
                  EventHandler(printer()))

解析:整個事件的處理如圖所示

image

【最終的組合】

比如,把xml改成json最后從中篩選的出固定信息.
buses.py

@coroutine
def buses_to_dicts(target):
    while True:
        event, value = (yield)
        # Look for the start of a <bus> element
        if event == 'start' and value[0] == 'bus':
            busdict = {}
            fragments = []
            # Capture text of inner elements in a dict
            while True:
                event, value = (yield)
                if event == 'start':
                    fragments = []
                elif event == 'text':
                    fragments.append(value)
                elif event == 'end':
                    if value != 'bus':
                        busdict[value] = "".join(fragments)
                    else:
                        target.send(busdict)
                        break

協程的一個有趣的事情是,您可以將初始數據源推送到低級別的語言,而不需要重寫所有處理階段。比如,PPT 中69-73頁介紹的,可以通過協程和低級別的語言進行聯動,從而達成非常好的優化效果。如Expat模塊或者cxmlparse模塊。
ps: ElementTree具有快速的遞增xml句法分析

第四部分:從數據處理到并發編程

復習一下上面學的特點:

協程有以下特點。

  • 協程和生成器非常像。
  • 我們可以用協程,去組合各種簡單的小組件。
  • 我們可以使用創建進程管道,數據流圖的方法去處理數據。
  • 你可以使用伴有復雜數據處理代碼的協程。

一個相似的主題:

我們往協程內傳送數據,向線程內傳送數據,也向進程內傳送數據。那么,協程自然很容易和線程和分布式系統聯系起來。

基礎的并發:

我們可以通過添加一個額外的層,從而封裝協程進入線程或者子進程。這描繪了幾個基本的概念。

image

目標!協程+線程【沒有蛀牙。

下面看一個線程的例子。
cothread.py

@coroutine
def threaded(target):
# 第一部分:
    messages = Queue()

    def run_target():
        while True:
            item = messages.get()
            if item is GeneratorExit:
                target.close()
                return
            else:
                target.send(item)

    Thread(target=run_target).start()
# 第二部分:
    try:
        while True:
            item = (yield)
            messages.put(item)
    except GeneratorExit:
        messages.put(GeneratorExit)

例子解析:第一部分:先新建一個隊列。然后定義一個永久循環的線程;這個線程可以將其中的元素拉出消息隊列,然后發送到目標里面。第二部分:接受上面送來的元素,并通過隊列,將他們傳送進線程里面。其中用到了GeneratorExit ,使得線程可以正確的關閉。

Hook up:cothread.py

if __name__ == '__main__':
    import xml.sax
    from cosax import EventHandler
    from buses import *

    xml.sax.parse("allroutes.xml", EventHandler(
        buses_to_dicts(
            threaded(
                filter_on_field("route", "22",
                filter_on_field("direction", "North Bound",
                                            bus_locations()))))))

image

但是:添加線程讓這個例子慢了50%

目標!協程+子進程

我們知道,進程之間是不共享系統資源的,所以要進行兩個子進程之間的通信,我們需要通過一個文件橋接兩個協程。

image
import cPickle as pickle
from coroutine import *

@coroutine
def sendto(f):
    try:
        while True:
            item = (yield)
            pickle.dump(item, f)
            f.flush()
    except StopIteration:
        f.close()

def recvfrom(f, target):
    try:
        while True:
            item = pickle.load(f)
            target.send(item)
    except EOFError:
        target.close()
# Example use
if __name__ == '__main__':
    import xml.sax
    from cosax import EventHandler
    from buses import *
    import subprocess
    p = subprocess.Popen(['python', 'busproc.py'],
                         stdin=subprocess.PIPE)
    xml.sax.parse("allroutes.xml",
                  EventHandler(
                      buses_to_dicts(
                          sendto(p.stdin))))

程序通過sendto()和recvfrom()傳遞文件。

和環境結合的協程:

使用協程,我們可以從一個任務的執行環境中剝離出他的實現。并且,協程就是那個實現。執行環境是你選擇的線程,子進程,網絡等。

需要注意的警告

  • 創建大量的協同程序,線程和進程可能是創建 不可維護 應用程序的一個好方法,并且會減慢你程序的速度。需要學習哪些是良好的使用協程的習慣。
  • 在協程里send()方法需要被適當的同步。
  • 如果你對已經正在執行了的協程使用send()方法,那么你的程序會發生崩潰。如:多個線程發送數據進入同一個協程。
  • 同樣的不能創造循環的協程:
image
  • 堆棧發送正在構建一種調用堆棧(send()函數不返回,直到目標產生)。
  • 如果調用一個正在發送進程的協程,將會拋出一個錯誤。
  • send() 函數不會掛起任何一個協程的執行。

第五部分:任務一樣的協程

Task的概念

在并發編程中,通常將問題細分為“任務”。
“任務”有下面幾個經典的特點:
* 擁有獨立的控制流。
* 擁有內在的狀態。
* 可以被安排規劃/掛起/恢復。
* 可與其他的任務通信。
協程也是任務的一種。

協程是任務的一種:

  1. 下面的部分 來告訴你協程有他自己的控制流,這里 if 的控制就是控制流。
@coroutine
def grep(pattern):
    print "Looking for %s" % pattern
    print "give a value in the coroutines"
    while True:
        line = (yield)
        if pattern in line:
            print line
  1. 協程是一個類似任何其他Python函數的語句序列。
  2. 協程有他們內在的自己的狀態,比如一些變量:其中的pattern和line就算是自己的狀態。
@coroutine
def grep(pattern):
    print "Looking for %s" % pattern
    print "give a value in the coroutines"
    while True:
        line = (yield)
        if pattern in line:
            print line
  1. 本地的生存時間和協程的生存時間相同。
  2. 很多協程構建了一個可執行的環境。
  3. 協程可以互相通信,比如:yield就是用來接受傳遞的信息,而上一個協程的send( )就是用來向下一個協程。
@coroutine
def grep(pattern):
    print "Looking for %s" % pattern
    print "give a value in the coroutines"
    while True:
        line = (yield)
        if pattern in line:
            print line
  1. 協程可以被掛起,重啟,關閉。
    • yield可以掛起執行進程。
    • send() 用來 重啟執行進程。
    • close()用來終止/關閉進程。

總之,一個協程滿足以上所有任務(task)的特點,所以協程非常像任務。但是協程不用與任何一個線程或者子進程綁定。

第六部分:操作系統的中斷事件。(微嵌課程學的好的同學可以直接跳到這部分的“啟示”??)

操作系統的執行(復習微嵌知識)

當計算機運行時,電腦沒有同時運行好幾條指令的打算。而無論是處理器,應用程序都不懂多任務處理。所以,操作系統需要去完成多任務的調度。操作系統通過在多個任務中快速切換來實現多任務。

需要解決的問題(還在復習微嵌知識)

CPU執行的是應用程序,而不是你的操作系統,那沒有被CPU執行的操作系統是怎么控制正在運行的應用程序中斷的呢。

中斷(interrupts)和陷阱(Traps)

操作系統只能通過兩個機制去獲得對應用程序的控制:中斷和陷阱。
* 中斷:和硬件有關的balabala。
* 陷阱:一個軟件發出的信號。
在兩種狀況下,CPU都會掛起正在做的,然后執行OS的代碼(這個時候,OS的代碼成功插入了應用程序的執行),此時,OS來切換了程序。

中斷的底層實現(略…碼字員微嵌只有70分???♀?)

中斷的高級表現:

* 中斷(Traps)使得OS的代碼可以實現。
* 在程序運行遇到中斷(Traps)時,OS強制在CPU上停止你的程序。
* 程序掛起,然后OS運行。

表現如下圖:


image

每次中斷(Traps)程序都會執行另一個不同的任務。

任務調度(非常簡單):

為了執行很多任務,添加一簇任務隊列。

image

啟示(很重要):

BB了這么多微嵌的內容,得到的是什么結論呢。類比任務調度,協程中yield聲明可以理解為中斷(Traps)。當一個生成器函數碰到了yield聲明,那函數將立即掛起。而執行被傳給生成器函數運行的任何代碼。如果你把yield聲明看成了一個中斷,那么你就可以組件一個多任務執行的操作系統了。

第七部分:讓我們建一個操作系統。【起飛了,請握好扶手

目標:滿足以下條件建成一個操作系統。

1. 用純python語句。
2. 不用線程。
3. 不用子進程。
4. 使用生成器和協程器。

我們用python去構建操作系統的一些動機:

* 尤其在存在線程鎖(GIL)的條件下,在線程間切換會變得非常重要。我要高并發!
* 不阻塞和異步I/O。我要高并發!
* 在實戰中可能會遇到:服務器要同時處理上千條客戶端的連接。我要高并發!
* 大量的工作 致力于實現 事件驅動 或者說 響應式模型。我要組件化!
* 綜上,python構建操作系統,有利于了解現在高并發,組件化的趨勢。

第一步:定義任務

定義一個任務類:任務像一個協程的殼,協程函數傳入target;任務類僅僅有一個run()函數。
pyos1.py

# Step 1: Tasks
# This object encapsulates a running task.

class Task(object):
    taskid = 0 # 所有task對象會共享這個值。不熟悉的朋友請補一下類的知識
    def __init__(self,target):
        Task.taskid += 1
        self.tid     = Task.taskid   # Task ID
        self.target  = target        # Target coroutine
        self.sendval = None          # Value to send

    # Run a task until it hits the next yield statement
    def run(self):
        return self.target.send(self.sendval)

任務類的執行:

if __name__ == '__main__':
    # A simple generator/coroutine function
    def foo():
        print "Part 1"
        yield
        print "Part 2"
        yield

    t1 = Task(foo())
    print "Running foo()"
    t1.run()
    print "Resuming foo()"
    t1.run()

在foo中,yield就像中斷(Traps)一樣,每次執行run(),任務就會執行到下一個yield(一個中斷)。

第二步:構建調度者

下面是調度者類,兩個屬性分別是Task隊列和task_id與Task類對應的map。schedule()向隊列里面添加Task。new()用來初始化目標函數(協程函數),將目標函數包裝在Task,進而裝入Scheduler。最后mainloop會從隊列里面拉出task然后執行到task的target函數的yield為止,執行完以后再把task放回隊列。這樣下一次會從下一個yield開始執行。
pyos2.py

from Queue import Queue

class Scheduler(object):
    def __init__(self):
        self.ready   = Queue()   
        self.taskmap = {}        

    def new(self,target):
        newtask = Task(target)
        self.taskmap[newtask.tid] = newtask
        self.schedule(newtask)
        return newtask.tid

    def schedule(self,task):
        self.ready.put(task)

    def mainloop(self):
        while self.taskmap:
            task = self.ready.get()
            result = task.run()
            self.schedule(task)

下面是一個執行的例子:

# === Example ===
if __name__ == '__main__':
    # Two tasks
    def foo():
        while True:
            print "I'm foo"
            yield
            print "I am foo 2"
            yield

    def bar():
        while True:
            print "I'm bar"
            yield
            print "i am bar 2"
            yield       
    # Run them
    sched = Scheduler()
    sched.new(foo())
    sched.new(bar())
    sched.mainloop()

執行結果,可以發現兩個task之間任務是交替的,并且以yield作為中斷點。每當執行撞到yield(中斷點)之后,Scheduler對Tasks做重新的規劃。下圖是兩個循環。
上述執行的結果:

image

第三步:確定任務的停止條件

如果,target函數里面不是死循環,那么上面的代碼就會出錯。所以我們對Scheduler做改進。添加一個從任務隊列中刪除的操作,和對于StopIteration的驗證。
【對scheduler做改進的原因是任務的性質:可以被安排規劃/掛起/恢復。】

class Scheduler(object):
    def __init__(self):
            ...     
    def new(self,target):
            ...
    def schedule(self,task):
            ...

    def exit(self,task):
        print "Task %d terminated" % task.tid
        del self.taskmap[task.tid]
    def mainloop(self):
         while self.taskmap:
            task = self.ready.get()
            try:
                result = task.run()
            except StopIteration:
                self.exit(task)
                continue
            self.schedule(task)

第四步:添加系統調用基類。

在OS中,中斷是應用程序請求系統服務的方式。在我們的代碼中,OS是調度者(scheduler),而中斷是yield。為了請求調度者服務,任務需要帶值使用yield聲明。
pyos4.py

class Scheduler(object):
      ...
    def mainloop(self):
        while self.taskmap:   # 1
            task = self.ready.get() 
            try:                 # 2
                result = task.run()
                if isinstance(result, SystemCall):
                    result.task = task
                    result.sched = self
                    result.handle()
                    continue
            except StopIteration:
                self.exit(task)
                continue 
            self.schedule(task) # 3

class SystemCall(object): # 4
    def handle(self):
        pass

代碼解析:
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

...
class GetTid(SystemCall):
    def handle(self):
        # 把task的id傳給task的返回參數:
        self.task.sendval = self.task.tid  
        # 再把task給放入Scheduler的隊列里面
        self.sched.schedule(self.task)

class Task(object):
      ...
    # Run a task until it hits the next yield statement
    def run(self):
        return self.target.send(self.sendval)

進行最后的調用:

if __name__ == '__main__':
    def foo():
        mytid = yield GetTid()
        for i in xrange(5):
            print "I'm foo", mytid
            yield
    def bar():
        mytid = yield GetTid()
        for i in xrange(10):
            print "I'm bar", mytid
            yield

    sched = Scheduler()
    sched.new(foo())
    sched.new(bar())
    sched.mainloop()

理解這段代碼的前提:(非常重要)
1. send()函數有返回值的,返回值是yield表達式右邊的值。在本段代碼中,result的返回值是yield GetTid()的GetTid的實例或者是yield后面的None。
2. 執行send(sendval)以后,sendval被傳入了yield表達式。并賦給了mytid,返回GetTid()給ruselt。

執行順序:
先創建一個調度者(Scheduler),然后在調度者里面添加兩個協程函數:foo(), bar(),最后觸發mainloop進行協程的調度執行。

系統調用原理:
系統調用是基于系統調用類實現的,如GetTid類,其目的是傳出自己的tid。傳出自己的tid之后,再將task放回隊列。

第五步:任務管理

上面我們搞定了一個GetTid系統調用。我們現在搞定更多的系統調用:
* 創建一個新的任務。
* 殺掉一個已經存在的任務。
* 等待一個任務結束。
這些細小的相同的操作會與線程,進程配合。

1. *創建一個新的系統調用*:通過系統調用加入一個task。
# Create a new task
class NewTask(SystemCall):
    def __init__(self,target):
        self.target = target
    def handle(self):
        tid = self.sched.new(self.target)
        self.task.sendval = tid
        self.sched.schedule(self.task)
2. *殺掉一個系統調用*:通過系統調用殺掉一個task。
class KillTask(SystemCall):
    def __init__(self, tid):
        self.tid = tid

    def handle(self):
        task = self.sched.taskmap.get(self.tid, None)
        if task:
            task.target.close()
            self.task.sendval = True
        else:
            self.task.sendval = False
        self.sched.schedule(self.task)
3. 進程等待:需要大幅度改進Scheduler。
class Scheduler(object):
    def __init__(self):
            ...
        # Tasks waiting for other tasks to exit
        self.exit_waiting = {}
    def new(self, target):
            ...
    def exit(self, task):
        print "Task %d terminated" % task.tid
        del self.taskmap[task.tid]
        # Notify other tasks waiting for exit
        for task in self.exit_waiting.pop(task.tid, []):
            self.schedule(task)
    def waitforexit(self, task, waittid):
        if waittid in self.taskmap:
            self.exit_waiting.setdefault(waittid, []).append(task)
            return True
        else:
            return False
    def schedule(self, task):
            ...
    def mainloop(self):
        ...

exit_waiting 是用來暫時存放要退出task的地方。

class WaitTask(SystemCall):
    def __init__(self, tid):
        self.tid = tid

    def handle(self):
        result = self.sched.waitforexit(self.task, self.tid)
        self.task.sendval = result
        # If waiting for a non-existent task,
        # return immediately without waiting
        if not result:
            self.sched.schedule(self.task)

設計討論:
* 在任務中引用另一個任務的唯一辦法 是 使用scheduler分配給它的任務ID。
* 上述準則是一個安全的封裝策略。
* 這個準則讓任務保持獨立,不與內核混淆在一起。
* 這個準則能讓所有的任務都被scheduler管理的好好的。

網絡服務器的搭建:

現在已經完成了:
* 多任務。
* 開啟新的進程。
* 進行新任務的管理。
這些特點都非常符合一個web服務器的各種特點。下面做一個Echo Server的嘗試。

from pyos6 import *
from socket import *
def handle_client(client, addr):
    print "Connection from", addr
    while True:
        data = client.recv(65536)
        if not data:
            break
        client.send(data)
    client.close()
    print "Client closed"
    yield  # Make the function a generator/coroutine
def server(port):
    print "Server starting"
    sock = socket(AF_INET, SOCK_STREAM)
    sock.bind(("", port))
    sock.listen(5)
    while True:
        client, addr = sock.accept()
        yield NewTask(handle_client(client, addr))
def alive():
    while True:
        print "I'm alive!"
        yield
sched = Scheduler()
sched.new(alive())
sched.new(server(45000))
sched.mainloop()

但問題是這個網絡服務器是I / O阻塞的。整個python的解釋器需要掛起,一直到I/O操作結束。

非阻塞的I/O

先額外介紹一個叫Select的模塊。select模塊可以用來監視一組socket鏈接的活躍狀態。用法如下:

reading = []    # List of sockets waiting for read
writing = []    # List of sockets waiting for write
# Poll for I/O activity

r,w,e = select.select(reading,writing,[],timeout)
    # r is list of sockets with incoming data
    # w is list of sockets ready to accept outgoing data
    # e is list of sockets with an error state

下面實現一個非阻塞I/O的網絡服務器,所用的思想就是之前所實現的Task waiting 思想。

class Scheduler(object):
    def __init__(self):
          ...
        # I/O waiting
        self.read_waiting = {}
        self.write_waiting = {}
      ...
    # I/O waiting
    def waitforread(self, task, fd):
        self.read_waiting[fd] = task

    def waitforwrite(self, task, fd):
        self.write_waiting[fd] = task

    def iopoll(self, timeout):
        if self.read_waiting or self.write_waiting:
            r, w, e = select.select(self.read_waiting,
                                    self.write_waiting, 
                                                [], timeout)
            for fd in r:
                    self.schedule(self.read_waiting.pop(fd))
            for fd in w:
                    self.schedule(self.write_waiting.pop(fd))

源碼解析:init里面的是兩個字典。用來存儲阻塞的IO的任務。waitforread()和waitforwrite()將需要等待寫入和等待讀取的task放在dict里面。這里的iopoll():使用select()去決定使用哪個文件描述器,并且能夠不阻塞任意一個和I/O才做有關系的任務。poll這個東西也可以放在mainloop里面,但是這樣會帶來線性的開銷增長。
詳情請見:Python Select 解析 - 金角大王 - 博客園

添加新的系統調用:

# Wait for a task to exit
class WaitTask(SystemCall):
    def __init__(self, tid):
        self.tid = tid

    def handle(self):
        result = self.sched.waitforexit(self.task, self.tid)
        self.task.sendval = result
        # If waiting for a non-existent task,
        # return immediately without waiting
        if not result:
            self.sched.schedule(self.task)
# Wait for reading
class ReadWait(SystemCall):
    def __init__(self, f):
        self.f = f

    def handle(self):
        fd = self.f.fileno()
        self.sched.waitforread(self.task, fd)
# Wait for writing
class WriteWait(SystemCall):
    def __init__(self, f):
        self.f = f

    def handle(self):
        fd = self.f.fileno()
        self.sched.waitforwrite(self.task, fd)

更多請見開頭那個連接里面的代碼:pyos8.py

這樣我們就完成了一個多任務處理的OS。這個OS可以并發執行,可以創建、銷毀、等待任務。任務可以進行I/O操作。并且最后我們實現了并發服務器。

第八部分:協程棧的一些問題的研究。

我們可能在使用yield的時候會遇到一些問題:

先來看一段示例代碼:

def Accept(sock):
      yield ReadWait(sock)
      return sock.accept()

def server(port):
        while True:
        client,addr = Accept(sock)
        yield NewTask(handle_client(client,addr))

這種情況下,server()函數里面的有調用Accept(),但是accept函數里面的yield不起作用。這是因為yield只能在函數棧的最頂層掛起一個協程。你也不能夠把yield寫進庫函數里面。
【這個限制是Stackless Python要解決的問題之一。

解決這個只能在函數棧頂掛起協程的解決方法。
* 有且只有一種方法,能夠創建可掛起的子協程和函數。
* 但是,創建可掛起的子協程和函數需要通過我們之前所說的Task Scheduler本身。
* 我們必須嚴格遵守yield聲明。
* 我們需要使用一種 -奇淫巧技- 叫做Trampolining(蹦床)。

讓我們來看看這個叫蹦床的奇淫巧技。

代碼:trampoline.py

def add(x, y):
    yield x + y

# A function that calls a subroutine
def main():
    r = yield add(2, 2)
    print r
    yield

def run():
    m = main()
    # An example of a "trampoline"
    sub = m.send(None)

    result = sub.send(None)
    m.send(result)

# execute:
run()

整個控制流如下:

image

我們看到,上圖中左側為統一的scheduler,如果我們想調用一個子線程,我們都用通過上面的scheduler進行調度。

控制流:

控制過程:
scheduler -> subroutine_1 -> scheduler -> subroutine_2 -> scheduler -> subroutine_1
就像蹦床(trampolining)一樣,所有的子進程調度都要先返回scheduler,再進行下一步。【有點像汽車換擋。

而不是:
-scheduler -> subroutine_1 -> subroutine_2 -> subroutine_1-
這種直接棧式的子協程調度是不被允許的。

第九部分:最后的一些話。

更加深遠的一些話題。

有很多更加深遠的話題值得我們去討論。其實在上面的套路里面都說了一些。
* 在task之間的通信。
* 處理阻塞的一些操作:比如和數據庫的一些鏈接。
* 多進程的協程和多線程的協程。
* 異常處理。

讓我們對yield一點小尊重:

Python 的生成器比很多人想象的有用的多。生成器可以:

* 定制可迭代對象。
* 處理程序管道和數據流。【第二部分提到】
* 事物處理。【第三部分提到的和SAX結合的事務處理】
* 合作的多任務處理【第四部分提到的Task,子進程子線程合作】

在下列三種蛀牙的情況下我們可以想起來,使用yield。

* 迭代器:要產生數據。
* 接受數據/消息:消費數據。
* 一個中斷:在合作性的多任務里面。

千萬不要一個函數里面包含兩個或多個以上的功能,比如函數是generator就是generator,是一個coroutine就是一個coroutin。

最后

感謝大家閱讀。我是LumiaXu。一名電子科大正在找實習的pythoner~。

更多訪問原作者的網站:
http://www.dabeaz.com

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,214評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,315評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,699評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,882評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,441評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,189評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,388評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,613評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,112評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,334評論 2 377

推薦閱讀更多精彩內容