在協程之前我們有什麼?
協程實際上不是一個新概念,作爲一個併發模型,在很早以前就能看到協程的身影。只是最近纔開始變得火熱起來,因爲它可以很好的處理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...]
......