Python 協程 - 1

在協程之前我們有什麼?

協程實際上不是一個新概念,作爲一個併發模型,在很早以前就能看到協程的身影。只是最近纔開始變得火熱起來,因爲它可以很好的處理IO密集型任務,而這符合互聯網行業的業務需求。
在我們重新認識協程之前,先簡短回顧下幾個常用的併發模型。
最簡單的就是串行執行的程序,遇到某一個IO事件就會阻塞直到IO事件返回,這種程序最大的缺點就是慢,耗時約等於全部IO耗時與計算耗時之和。
爲了提升運行效率,可以採用多線程或多進程模型,因爲IO事件實際上是不消耗計算資源的,只需要等待而已,所以在IO事件等待的時候切換到另一個任務來提升運行速度,而這需要依靠操作系統對線程和進程的調度。
但是線程和進程的分配是需要開銷的,在面對大量IO事件時,系統資源就不夠用了,所以纔有了 select, poll, epoll等非阻塞異步IO。但是異步程序雖然性能上非常棒,但是可讀性上十分反人類,因爲它對原本連貫的邏輯進行了拆分,在系統規模變得很大時,我們很難理解系統的邏輯。而協程在一定程度上可以解決這個問題,它可以在保持程序的邏輯連貫性的同時,通過對任務的調度來實現調用的異步性,同時協程由於整個程序都在一個線程內,所以上下文切換的開銷極小,運行效率極高。

Python 中的協程

生成器

生成器是 Python 中的一個重要特性,在 Python2 裏面協程需要在生成器的基礎上進行拓展。所以先來看一個生成器的例子,這個例子是一個將大文件分塊讀入內存的例子,它避免了文件過大無法一次性讀入內存,或者是一次性讀入太慢的問題。常用的 range 也有一個生成器版 xrange,range 一次性會生成所有的數據,而 xrange則到需要時才生成。

import os


def chunked_file(file, n=100):
    with open(file) as fp:
         while True:
             chunk = fp.read(n)
             if chunk:
                 yield chunk.encode('utf-8')
             else:
                 break
             fp.seek(n, os.SEEK_CUR)

if __name__ == '__main__':
    for chunk in chunked_file('demo.txt', 256):
        print chunk                    

除了上面的用法,生成器還可以主動調用 next 方法獲取下一個輸出,在沒有下一個輸出的情況下會拋出 StopIteration , 前面的for循環只是一個語法糖幫我們處理了next和對拋出異常的檢測。

>>> demo = chunked_file('demo.txt', 10)
>>> demo.next()
'1234567890'
>>> demo.next()
'1234567890'
...
>>> demo.next()
StopIteration: 

傳遞數據

Python 裏爲生成器提供了一個 send 方法,用來向生成器發送數據。下面是一個echo函數的例子,同時可以通過調用生成器函數的close方法來關閉生成器。

# echo.py
def echo():
    x = yield 
    while True:
        x = yield x
>>> generator = echo()
>>> generator.send(None) # 啓動生成器
>>> generator.send('hello')
'hello'
>>> generator.send('world')
'world'
>>> generator.close() # 關閉生成器

使用 yield 和 send 構建協程

有了這兩個基礎的語義,就可以在此基礎上構建協程了。下面是一個簡單的協程調度實現,通過對函數的切換達到併發執行的效果。

# -*- encoding=utf-8 -*-
import Queue


def countdown(n):
    while n > 0:
        print '[Counting Down {n}...]'.format(n=n)
        n -= 1
        yield


def countup(n):
    x = 0
    while x < n:
        print '[Counting Up {n}...]'.format(n=x)
        x += 1
        yield


def scheduler(fn_list=(countdown(10), countdown(5), countup(5))):
    queue = Queue.deque()
    queue.extend(fn_list)
    while len(queue):
        try:
            task = queue.popleft()
            task.send(None)
            queue.append(task)
        except StopIteration:
            pass


if __name__ == '__main__':
    scheduler()

輸出結果:

[Counting Down 10...]
[Counting Down 5...]
[Counting Up 0...]
[Counting Down 9...]
[Counting Down 4...]
[Counting Up 1...]
[Counting Down 8...]
[Counting Down 3...]
[Counting Up 2...]
......

參考鏈接

編程珠璣番外篇-Q 協程的歷史,現在和未來

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章