深刻理解python協程


概述

因爲 cpu和 磁盤讀寫的 效率有很大的差距,每每cpu執行代碼,而後遇到須要從磁盤中讀寫文件的操做,此時主線程會中止運行,等待IO操做完成後再繼續進行,這要就致使cpu的利用率很是的低。服務器

協程能夠實現單線程同時執行多個任務,可是須要本身手動的經過send函數和yield關鍵字配合來傳遞消息,asyncio模塊可以自動幫咱們傳遞消息。app

python中協程主要經歷了以下三個階段dom

1)生成器變形 yield/send異步

2)asyncio.coroutine和yield fromsocket

3)async/await關鍵字async


生成器變形 yield/send

yield

Python中函數若是把return換成了yield,那麼這個函數就再也不普通函數了,而是一個生成器函數

簡單生成器示例:oop

def mygen(alist):   # define a generator
    while alist:
        c = alist.pop()
        yield c


lst = [1, 2, 3]
g = mygen(lst)  # get a generator object
print(g)        # <generator object mygen at 0x0000020225555F10>

while True:
    try:
        print(next(g))  # 3 2 1
    except StopIteration:
        break


生成器本質上也是迭代器,所以不只可使用next()取值,還可使用for循環取值編碼

for item in g:
    print(item)     # 3 2 1


send

生成器函數最大的特色是能夠接收一個外部傳入的變量,並根據變量內容計算結果後返回,這個特色是根據send()函數實現的

send()函數使用示例:

def gen():
    value = 0
    while True:
        receive = yield value
        if receive == "Q" or receive == "q":
            break
        value = "got:%s" % receive


g = gen()
print(g.send(None))     # 第一個必須是None,不然會報錯
print(g.send("hello~"))
print(g.send(123))
print(g.send([1, 2, 3]))


執行結果

0
got:hello~
got:123
got:[1, 2, 3]

注意:第一個send()裏傳入的變量必須是None,不然會報錯TypeError: can't send non-None value to a just-started generator



這裏最關鍵的一步就是receive = yield value,這一句實際上分爲三步

1)向函數外拋出(返回)value

2)暫停,等待next()或send()恢復

3)將等號右邊的表達式的值(這個值是傳入的)賦值給receive



下面來梳理一下執行流程

1)經過g.send(None)或者next(g)啓動生成器函數,並執行到第一個yield的位置

2)執行yield value,程序返回value,也就是0,以後暫停,等待下一個next()或send(),注意這時並無給receive賦值

3)gen返回value以後跳出,執行主程序裏面的g.send("hello~"),執行這一句會傳入"hello~",從以前暫停的位置繼續執行,也就是賦值給receive,繼續往下執行,value變成"got:hello~",而後判斷while,執行到yield value,返回value,因此打印出"got:hello~",以後進入暫停,等待下一個send()激活

4)後續的g.send(123)執行流程相似,若是傳入"q",gen會執行到break,整個函數執行完畢,會得StopIteration



從上面能夠看出,在第一次send(None)啓動生成器(執行1>2,一般第一次返回的值並無什麼用)以後,對於外部的每一次send(),生成器的實際在循環中的運行順序是3–>1–>2,也就是先獲取值,而後do something,而後返回一個值,再暫停等待。


yield from

yield from是Python3.3引入的,先來看一段代碼

def gen1():
    yield range(5)


def gen2():
    yield from range(5)


iter1 = gen1()
iter2 = gen2()

for item in iter1:
    print(item)

for item in iter2:
    print(item)


執行結果

range(0, 5)
0
1
2
3
4

從上面的示例能夠看出來yield是將range這個可迭代對象直接返回,而yield from解析range對象,將其中每個item返回,yield from本質上等於

for item in iterable:
    yield item



注意yield from後面只能接可迭代對象



下面來看一個例子,咱們編寫一個斐波那契數列函數

def fab(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a+b
        n += 1


f = fab(5)

fab不是一個普通函數,而是一個生成器。所以fab(5)並無執行函數,而是返回一個生成器對象,假設要在fab()的基礎上實現一個函數,調用起始都要記錄日誌

def wrapper(func_iter):
    print("start")
    for item in func_iter:
        yield item
    print("end")


wrap = wrapper(fab(5))
for i in wrap:
    print(i)



下面使用yield from代替for循環

def wrapper(func_iter):
    print("start")
    yield from func_iter
    print("end")


wrap = wrapper(fab(5))
for i in wrap:
    print(i)

asyncio.coroutine和yield from

yield from在asyncio模塊(python3.4引入)中得以發揚光大。以前都是手動的經過send函數和yield關鍵字配合來傳遞消息,如今當聲明函數爲協程後,咱們經過事件循環來調度協程

import asyncio, random


@asyncio.coroutine      # 將一個generator定義爲coroutine
def smart_fib(n):
    i, a, b = 0, 0, 1
    while i < n:
        sleep_time = random.uniform(0, 0.2)
        yield from asyncio.sleep(sleep_time)    # 一般yield from後都是接的耗時操做
        print("smart take %s secs to get %s" % (sleep_time, b))
        a, b = b, a+b
        i += 1


@asyncio.coroutine
def stupid_fib(n):
    i, a, b = 0, 0, 1
    while i < n:
        sleep_time = random.uniform(0, 0.5)
        yield from asyncio.sleep(sleep_time)
        print("stupid take %s secs to get %s" % (sleep_time, b))
        a, b = b, a+b
        i += 1


if __name__ == '__main__':
    loop = asyncio.get_event_loop()     # 獲取事件循環的引用
    tasks = [                           # 建立任務列表
        smart_fib(10),
        stupid_fib(10),
    ]
    loop.run_until_complete(asyncio.wait(tasks))    # wait會分別把各個協程包裝進一個Task 對象。
    print("All fib finished")
    loop.close()


yield from語法可讓咱們方便地調用另外一個generator。 本例中yield from後面接的asyncio.sleep()也是一個coroutine(裏面也用了yield from),因此線程不會等待asyncio.sleep(),而是直接中斷並執行下一個消息循環。當asyncio.sleep()返回時,線程就能夠從yield from拿到返回值(此處是None),而後接着執行下一行語句。


asyncio是一個基於事件循環的實現異步I/O的模塊。經過yield from,咱們能夠將協程asyncio.sleep的控制權交給事件循環,而後掛起當前協程;以後,由事件循環決定什麼時候喚醒asyncio.sleep,接着向後執行代碼。
協程之間的調度都是由事件循環決定。



yield from asyncio.sleep(sleep_secs) 這裏不能用time.sleep(1)由於time.sleep()返回的是None,它不是iterable,還記得前面說的yield from後面必須跟iterable對象(能夠是生成器,迭代器)。



另外一個示例

import asyncio


@asyncio.coroutine
def wget(host):
    print('wget %s...' % host)
    connect = asyncio.open_connection(host, 80)  # 與要獲取數據的網頁創建鏈接
    # 鏈接中包含一個 reader和writer
    reader, writer = yield from connect  # 經過writer向服務器發送請求,經過reader讀取服務器repnse回來的請求
    header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host  # 組裝請求頭信息
    writer.write(header.encode('utf-8'))  # 須要對請求頭信息進行編碼
    yield from writer.drain()  # 因爲writer中有緩衝區,若是緩衝區沒滿不且drain的話數據不會發送出去
    while True:
        line = yield from reader.readline()  # 返回的數據放在了reader中,經過readline一行一行地讀取數據
        if line == b'\r\n':  # 由於readline實際上已經把\r\n轉換成換行了,而此時又出現\r\n說明之前有連續兩組\r\n
            break  # 即\r\n\r\n,因此下面就是response body了
        print('%s header > %s' % (host, line.decode('utf-8').rstrip()))
    # Ignore the body, close the socket
    writer.close()
    # reader.close()   AttributeError: 'StreamReader' object has no attribute 'close'


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.163.com']]
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()


async和await

弄清楚了asyncio.coroutine和yield from以後,在Python3.5中引入的async和await就不難理解了,咱們使用的時候只須要把@asyncio.coroutine換成async,把yield from換成await就能夠了。固然,從Python設計的角度來講,async/await讓協程表面上獨立於生成器而存在,將細節都隱藏於asyncio模塊之下,語法更清晰明瞭。

加入新的關鍵字 async ,能夠將任何一個普通函數變成協程

一個簡單的示例

import time, asyncio, random

async def mygen(alist):
    while alist:
        c = alist.pop()
        print(c)


lst = [1, 2, 3]
g = mygen(lst)
print(g)



執行結果

<coroutine object mygen at 0x00000267723FB3B8>      # 協程對象
sys:1: RuntimeWarning: coroutine 'mygen' was never awaited



能夠看到,咱們在前面加上async,該函數就變成了一個協程,可是async對生成器是無效的

async def mygen(alist):
    while alist:
        c = alist.pop()
        yield c

lst = [1, 2, 3]
g = mygen(lst)
print(g)



執行結果

<async_generator object mygen at 0x000001540EF505F8>    # 並非協程對象



因此正常的協程是這樣的

import time, asyncio, random

async def mygen(alist):
    while alist:
        c = alist.pop()
        print(c)
        await asyncio.sleep(1)


lst1 = [1, 2, 3]
lst2 = ["a", "b", "c"]
g1 = mygen(lst1)
g2 = mygen(lst2)



要運行協程,要用事件循環
在上面的代碼下面加上:

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    tasks = [
        c1,
        c2
    ]
    loop.run_until_complete(asyncio.wait(tasks))
    print("all finished")
    loop.close()



參考:

1)https://blog.csdn.net/soonfly/article/details/78361819

2)https://blog.csdn.net/weixin_40247263/article/details/82728437

相關文章
相關標籤/搜索