【妙用協程】 – 協程當然可以用來處理I/O阻塞問題

NO IMAGE

前面講的兩個協程的用途,一個是用來使用協程表達工作流裡的流程的概念,一個是用協程來表達一個動畫的播放過程。總結起來,就是用協程來解決帶有流程阻塞的程式碼邏輯組織的問題。但是協程並不是生來幹這個的,協程最主流的用途是一種用於處理I/O阻塞的工具。I/O阻塞的挑戰是多重的,一方面是有很多人研究高併發低延遲之類的問題,另外一方面I/O阻塞對於程式碼邏輯的組織挑戰也是巨大的。比如最經典的基於回撥的I/O阻塞編碼風格,下面是一個使用Twisted框架的程式碼示例(摘自:https://github.com/feihong/tulip-talk/blob/master/examples):

from twisted.internet import reactor
from twisted.internet.defer import Deferred, succeed
from twisted.internet.protocol import Protocol
from twisted.web.client import Agent

def print_headers(response):
    for k, v in response.headers.getAllRawHeaders():
        print('{}: {}'.format(k, v[0][:80]))

    return get_response_body(response)

def get_response_body(response):
    class BodyReceiver(Protocol):
        def dataReceived(self, data):
            chunks.append(data)
        def connectionLost(self, reason):
            finished.callback(''.join(chunks))

    finished = Deferred()
    chunks = []
    response.deliverBody(BodyReceiver())
    return finished

def print_body(data):
    print('\nReceived {} bytes.\n'.format(len(data)))
    return succeed(None)

if __name__ == '__main__':
    agent = Agent(reactor)
    d = agent.request('GET', 'http://megafeihong.tumblr.com')
    d.addCallback(print_headers)
    d.addCallback(print_body)
    d.addCallback(lambda x: reactor.stop())
    reactor.run()

程式碼邏輯是不是非常不好懂?究其原因在於一個順序的執行過程,因為中間的I/O阻塞被拆成了義大利麵條式的破碎邏輯。而是用協程就可以很好的實現Logic Locality,把上下相關的程式碼放在一段函式內:

import tulip
from tulip import http

@tulip.coroutine
def download(url):
    response = yield from http.request('GET', url)
    for k, v in response.items():
        print('{}: {}'.format(k, v[:80]))

    data = yield from response.read()
    print('\nReceived {} bytes.\n'.format(len(data)))

if __name__ == '__main__':
    loop = tulip.get_event_loop()
    coroutine = download('http://omegafeihong.tumblr.com')
    loop.run_until_complete(coroutine)

這段程式碼是不是容易懂多了?這個框架叫tulip,是Python基於協程,確切說是基於generator的網路非同步I/O程式設計框架。其中使用了一個新的yield from語法,暫時不用深究,基本上和yield是一個意思,代表函式在這個地方放棄執行,讓外部去把I/O處理完,等I/O操作完成了再從上次放棄執行的地方繼續執行後面的語句。