Python黑魔法之協程/異步IO

chl1988 7年前發布 | 14K 次閱讀 IO Python Python開發

協程與異步IO

引言

隨著node.js的盛行,相信大家今年多多少少都聽到了異步編程這個概念。Python社區雖然對于異步編程的支持相比其他語言稍顯遲緩,但是也在Python3.4中加入了 asynico ,在Python3.5上又提供了async/await語法層面的支持,剛正式發布的 Python3.6 中asynico也已經由臨時版改為了穩定版。下面我們就基于 Python3.4+ 來了解一下異步編程的概念以及asyncio的用法。

什么是協程

通常在Python中我們進行并發編程一般都是使用多線程或者多進程來實現的,對于計算型任務由于GIL的存在我們通常使用多進程來實現,而對與IO型任務我們可以通過線程調度來讓線程在執行IO任務時讓出GIL,從而實現表面上的并發。

其實對于IO型任務我們還有一種選擇就是協程, 協程是運行在單線程當中的“并發” ,協程相比多線程一大優勢就是省去了多線程之間的切換開銷,獲得了更大的運行效率。Python中的asyncio也是基于協程來進行實現的。在進入asyncio之前我們先來了解一下Python中怎么通過生成器進行協程來實現并發。

example1

我們先來看一個簡單的例子來了解一下什么是協程(coroutine),對生成器不了解的朋友建議先看一下 Stackoverflow上面的這篇高票提問 。

>>> def coroutine():
...     reply = yield 'hello'
...     yield reply
... 

>>> c = coroutine()

>>> next(c)
'hello'

>>> c.send('world')
'world'

example2

下面這個程序我們要實現的功能就是 模擬多個學生同時向一個老師提交作業 ,按照傳統的話我們或許要采用多線程/多進程,但是這里我們可以采用生成器來實現協程用來模擬并發。

如果下面這個程序讀起來有點困難,可以直接跳到后面部分,并不影響閱讀,等你理解協程的本質,回過頭來看就很簡單了。

from collections import deque

def student(name, homeworks):
    for homework in homeworks.items():
        yield (name, homework[0], homework[1])  # 學生"生成"作業給老師

class Teacher(object):
    def __init__(self, students):
        self.students = deque(students)

    def handle(self):
        """老師處理學生的作業"""
        while len(self.students):
            student = self.students.pop()
            try:
                homework = next(student)
                print('handling', homework[0], homework[1], homework[2])
            except StopIteration:
                pass
            else:
                self.students.appendleft(student)

下面我們來調用一下這個程序。

Teacher([
    student('Student1', {'math': '1+1=2', 'cs': 'operating system'}),
    student('Student2', {'math': '2+2=4', 'cs': 'computer graphics'}),
    student('Student3', {'math': '3+3=5', 'cs': 'compiler construction'})
]).handle()

這是輸出結果,我們僅僅只用了一個簡單的生成器就實現了并發(concurrence),注意不是并行(parallel),因為我們的程序僅僅是運行在一個單線程當中。

handling Student3 cs compiler construction
handling Student2 cs computer graphics
handling Student1 cs operating system
handling Student3 math 3+3=5
handling Student2 math 2+2=4
handling Student1 math 1+1=2

使用asyncio模塊實現協程

從Python3.4開始asyncio模塊加入到了標準庫,通過asyncio我們可以輕松實現協程來完成異步IO操作。

解釋一下下面這段代碼,我們創造了一個協程 display_date(num, loop) ,然后它使用 yield from 關鍵字來等待 asyncio.sleep(2) 的返回結果,對于asnycio.sleep(2)可以把它理解為一個IO操作,實際上它內部也是一個協程,但是要等待2s的時間后才返回結果。而在這等待的2s之間它會讓出CPU的執行權,直到asyncio.sleep(2)返回結果。

# coroutine.py
import asyncio
import datetime

@asyncio.coroutine  # 定義一個協程
def display_date(num, loop):
    end_time = loop.time() + 10.0
    while True:
        print("Loop: {} Time: {}".format(num, datetime.datetime.now()))
        if (loop.time() + 1.0) >= end_time:
            break
        yield from asyncio.sleep(2)  # 這里可以理解為一個2s的IO中斷

loop = asyncio.get_event_loop()  # 獲取一個event_loop

tasks = [
asyncio.ensure_future(display_date(1, loop)),  # 開啟協程1
asyncio.ensure_future(display_date(2, loop))  # 開啟協程2
]

loop.run_until_complete(asyncio.gather(*tasks))
loop.close()

下面是運行結果,注意到并發的效果沒有,程序從開始到結束只用大約10s,而在這里我們并沒有使用任何的多線程/多進程代碼。在實際項目中你可以將asyncio.sleep(secends)替換成相應的IO任務,比如數據庫/磁盤文件讀寫等操作。

ziwenxie :: ~ ? python coroutine.py
Loop: 1 Time: 2016-12-19 16:06:46.515329
Loop: 2 Time: 2016-12-19 16:06:46.515446
Loop: 1 Time: 2016-12-19 16:06:48.517613
Loop: 2 Time: 2016-12-19 16:06:48.517724
Loop: 1 Time: 2016-12-19 16:06:50.520005
Loop: 2 Time: 2016-12-19 16:06:50.520169
Loop: 1 Time: 2016-12-19 16:06:52.522452
Loop: 2 Time: 2016-12-19 16:06:52.522567
Loop: 1 Time: 2016-12-19 16:06:54.524889
Loop: 2 Time: 2016-12-19 16:06:54.525031
Loop: 1 Time: 2016-12-19 16:06:56.527713
Loop: 2 Time: 2016-12-19 16:06:56.528102

在Python3.5中為我們提供更直接的對協程的支持,引入了async/await關鍵字,上面的代碼我們可以這樣改寫,使用async代替了@asyncio.coroutine,使用了await代替了yield from,這樣我們的代碼變得更加簡潔可讀。

import asyncio
import datetime

async def display_date(num, loop):
    end_time = loop.time() + 10.0
    while True:
        print("Loop: {} Time: {}".format(num, datetime.datetime.now()))
        if (loop.time() + 1.0) >= end_time:
            break
        await asyncio.sleep(2)

loop = asyncio.get_event_loop()  # 獲取一個event_loop

tasks = [
asyncio.ensure_future(display_date(1, loop)),  # 開啟協程1
asyncio.ensure_future(display_date(2, loop))  # 開啟協程2
]

loop.run_until_complete(asyncio.gather(*tasks))
loop.close()

在爬蟲中使用asyncio來實現異步IO

下面我們來通過一個例子來了解怎么在Python爬蟲項目中使用asyncio。

example1

import asyncio
import requests

async def spider(loop):
    # 調用request庫的get方法,返回一個future對象
    future1 = loop.run_in_executor(None, requests.get, 'https://www.python.org/')
    future2 = loop.run_in_executor(None, requests.get, 'http://httpbin.org/')
    # await future對象,實際上就是一個網絡IO延遲
    response1 = await future1
    response2 = await future2
    print(response1.text)
    print(response2.text)

loop = asyncio.get_event_loop()  # 得到一個event_loop
loop.run_until_complete(spider(loop))

關于future對象,asyncio中的futrue和 標準庫中的concurrent.future 用法基本一致,建議大家去詳細了解一下,我晚點再來更新。

文章未完,待更新。

References

DOCUMENTION OF ASYNCIO

COROUTINES AND ASYNC/AWAIT

STACKOVERFLOW

PyMOTW-3

 

來自:http://www.jianshu.com/p/4e048726b613

 

 本文由用戶 chl1988 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!