理解Python的協程(Coroutine)

由於GIL的存在,導致Python多線程性能甚至比單線程更糟。

GIL: 全局解釋器鎖(英語:Global Interpreter Lock,縮寫GIL),是計算機程序設計語言解釋器用於同步線程的一種機制,它使得任何時刻僅有一個線程在執行。[1]即便在多核心處理器上,使用 GIL 的解釋器也只允許同一時間執行一個線程。

於是出現了協程(Coroutine)這麼個東西。

協程: 協程,又稱微線程,纖程,英文名Coroutine。協程的作用,是在執行函數A時,可以隨時中斷,去執行函數B,然後中斷繼續執行函數A(可以自由切換)。但這一過程並不是函數調用(沒有調用語句),這一整個過程看似像多線程,然而協程只有一個線程執行.

協程由於由程序主動控制切換,沒有線程切換的開銷,所以執行效率極高。對於IO密集型任務非常適用,如果是cpu密集型,推薦多進程+協程的方式。

在Python3.4之前,官方沒有對協程的支持,存在一些三方庫的實現,比如gevent和Tornado。3.4之後就內置了asyncio標準庫,官方真正實現了協程這一特性。

而Python對協程的支持,是通過Generator實現的,協程是遵循某些規則的生成器。因此,我們在瞭解協程之前,我們先要學習生成器。

生成器(Generator)

我們這裏主要討論yieldyield from這兩個表達式,這兩個表達式和協程的實現息息相關。

  • Python2.5中引入yield表達式,參見PEP342
  • Python3.3中增加yield from語法,參見PEP380

方法中包含yield表達式後,Python會將其視作generator對象,不再是普通的方法。

yield表達式的使用

我們先來看該表達式的具體使用:

def test():
    print("generator start")
    n = 1
    while True:
        yield_expression_value = yield n
        print("yield_expression_value = %d" % yield_expression_value)
        n += 1


# ①創建generator對象
generator = test()
print(type(generator))

print("\n---------------\n")

# ②啓動generator
next_result = generator.__next__()
print("next_result = %d" % next_result)

print("\n---------------\n")

# ③發送值給yield表達式
send_result = generator.send(666)
print("send_result = %d" % send_result)

執行結果:

<class 'generator'>

---------------

generator start
next_result = 1

---------------

yield_expression_value = 666
send_result = 2

方法說明:

  • __next__()方法: 作用是啓動或者恢復generator的執行,相當於send(None)

  • send(value)方法:作用是發送值給yield表達式。啓動generator則是調用send(None)

執行結果的說明:

  • ①創建generator對象:包含yield表達式的函數將不再是一個函數,調用之後將會返回generator對象

  • ②啓動generator:使用生成器之前需要先調用__next__或者send(None),否則將報錯。啓動generator後,代碼將執行到yield出現的位置,也就是執行到yield n,然後將n傳遞到generator.__next__()這行的返回值。(注意,生成器執行到yield n後將暫停在這裏,直到下一次生成器被啓動)

  • ③發送值給yield表達式:調用send方法可以發送值給yield表達式,同時恢復生成器的執行。生成器從上次中斷的位置繼續向下執行,然後遇到下一個yield,生成器再次暫停,切換到主函數打印出send_result。

理解這個demo的關鍵是:生成器啓動或恢復執行一次,將會在yield處暫停。上面的第②步僅僅執行到了yield n,並沒有執行到賦值語句,到了第③步,生成器恢復執行纔給yield_expression_value賦值。

生產者和消費者模型

上面的例子中,代碼中斷-->切換執行,體現出了協程的部分特點。

我們再舉一個生產者、消費者的例子,這個例子來自廖雪峯的Python教程

傳統的生產者-消費者模型是一個線程寫消息,一個線程取消息,通過鎖機制控制隊列和等待,但一不小心就可能死鎖。

現在改用協程,生產者生產消息後,直接通過yield跳轉到消費者開始執行,待消費者執行完畢後,切換回生產者繼續生產,效率極高。

def consumer():
    print("[CONSUMER] start")
    r = 'start'
    while True:
        n = yield r
        if not n:
            print("n is empty")
            continue
        print("[CONSUMER] Consumer is consuming %s" % n)
        r = "200 ok"


def producer(c):
    # 啓動generator
    start_value = c.send(None)
    print(start_value)
    n = 0
    while n < 3:
        n += 1
        print("[PRODUCER] Producer is producing %d" % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    # 關閉generator
    c.close()


# 創建生成器
c = consumer()
# 傳入generator
producer(c)

執行結果:

[CONSUMER] start
start
[PRODUCER] producer is producing 1
[CONSUMER] consumer is consuming 1
[PRODUCER] Consumer return: 200 ok
[PRODUCER] producer is producing 2
[CONSUMER] consumer is consuming 2
[PRODUCER] Consumer return: 200 ok
[PRODUCER] producer is producing 3
[CONSUMER] consumer is consuming 3
[PRODUCER] Consumer return: 200 ok

注意到consumer函數是一個generator,把一個consumer傳入produce後:

  1. 首先調用c.send(None)啓動生成器;
  1. 然後,一旦生產了東西,通過c.send(n)切換到consumer執行;
  1. consumer通過yield拿到消息,處理,又通過yield把結果傳回;
  1. produce拿到consumer處理的結果,繼續生產下一條消息;
  1. produce決定不生產了,通過c.close()關閉consumer,整個過程結束。

整個流程無鎖,由一個線程執行,produceconsumer協作完成任務,所以稱爲“協程”,而非線程的搶佔式多任務。

yield from表達式

Python3.3版本新增yield from語法,新語法用於將一個生成器部分操作委託給另一個生成器。此外,允許子生成器(即yield from後的“參數”)返回一個值,該值可供委派生成器(即包含yield from的生成器)使用。並且在委派生成器中,可對子生成器進行優化。

我們先來看最簡單的應用,例如:

# 子生成器
def test(n):
    i = 0
    while i < n:
        yield i
        i += 1

# 委派生成器
def test_yield_from(n):
    print("test_yield_from start")
    yield from test(n)
    print("test_yield_from end")


for i in test_yield_from(3):
    print(i)

輸出:

test_yield_from start
0
1
2
test_yield_from end

這裏我們僅僅給這個生成器添加了一些打印,如果是正式的代碼中,你可以添加正常的執行邏輯。

如果上面的test_yield_from函數中有兩個yield from語句,將串行執行。比如將上面的test_yield_from函數改寫成這樣:

def test_yield_from(n):
    print("test_yield_from start")
    yield from test(n)
    print("test_yield_from doing")
    yield from test(n)
    print("test_yield_from end")

將輸出:

test_yield_from start
0
1
2
test_yield_from doing
0
1
2
test_yield_from end

在這裏,yield from起到的作用相當於下面寫法的簡寫形式

for item in test(n):
    yield item

看起來這個yield from也沒做什麼大不了的事,其實它還幫我們處理了異常之類的。具體可以看stackoverflow上的這個問題:In practice, what are the main uses for the new “yield from” syntax in Python 3.3?

協程(Coroutine)

  • Python3.4開始,新增了asyncio相關的API,語法使用@asyncio.coroutineyield from實現協程
  • Python3.5中引入async/await語法,參見PEP492

我們先來看Python3.4的實現。

@asyncio.coroutine

Python3.4中,使用@asyncio.coroutine裝飾的函數稱爲協程。不過沒有從語法層面進行嚴格約束。

對裝飾器不瞭解的小夥伴可以看我的上一篇博客--《理解Python裝飾器》

對於Python原生支持的協程來說,Python對協程和生成器做了一些區分,便於消除這兩個不同但相關的概念的歧義:

  • 標記了@asyncio.coroutine裝飾器的函數稱爲協程函數,iscoroutinefunction()方法返回True
  • 調用協程函數返回的對象稱爲協程對象,iscoroutine()函數返回True

舉個栗子,我們給上面yield from的demo中添加@asyncio.coroutine

import asyncio

...

@asyncio.coroutine
def test_yield_from(n):
    ...

# 是否是協程函數
print(asyncio.iscoroutinefunction(test_yield_from))
# 是否是協程對象
print(asyncio.iscoroutine(test_yield_from(3)))

毫無疑問輸出結果是True。

可以看下@asyncio.coroutine的源碼中查看其做了什麼,我將其源碼簡化下,大致如下:

import functools
import types
import inspect

def coroutine(func):
    # 判斷是否是生成器
    if inspect.isgeneratorfunction(func):
        coro = func
    else:
        # 將普通函數變成generator
        @functools.wraps(func)
        def coro(*args, **kw):
            res = func(*args, **kw)
            res = yield from res
            return res
    # 將generator轉換成coroutine
    wrapper = types.coroutine(coro)
    # For iscoroutinefunction().
    wrapper._is_coroutine = True
    return wrapper

將這個裝飾器標記在一個生成器上,就會將其轉換成coroutine。

然後,我們來實際使用下@asyncio.coroutineyield from

import asyncio

@asyncio.coroutine
def compute(x, y):
    print("Compute %s + %s ..." % (x, y))
    yield from asyncio.sleep(1.0)
    return x + y

@asyncio.coroutine
def print_sum(x, y):
    result = yield from compute(x, y)
    print("%s + %s = %s" % (x, y, result))

loop = asyncio.get_event_loop()
print("start")
# 中斷調用,直到協程執行結束
loop.run_until_complete(print_sum(1, 2))
print("end")
loop.close()

執行結果:

start
Compute 1 + 2 ...
1 + 2 = 3
end

print_sum這個協程中調用了子協程compute,它將等待compute執行結束才返回結果。

這個demo點調用流程如下圖:

EventLoop將會把print_sum封裝成Task對象

流程圖展示了這個demo的控制流程,不過沒有展示其全部細節。比如其中“暫停”的1s,實際上創建了一個future對象, 然後通過BaseEventLoop.call_later()在1s後喚醒這個任務。

值得注意的是,@asyncio.coroutine將在Python3.10版本中移除。

async/await

Python3.5開始引入async/await語法(PEP 492),用來簡化協程的使用並且便於理解。

async/await實際上只是@asyncio.coroutineyield from的語法糖:

  • @asyncio.coroutine替換爲async
  • yield from替換爲await

即可。

比如上面的例子:

import asyncio


async def compute(x, y):
    print("Compute %s + %s ..." % (x, y))
    await asyncio.sleep(1.0)
    return x + y


async def print_sum(x, y):
    result = await compute(x, y)
    print("%s + %s = %s" % (x, y, result))


loop = asyncio.get_event_loop()
print("start")
loop.run_until_complete(print_sum(1, 2))
print("end")
loop.close()

我們再來看一個asyncio中Future的例子:

import asyncio

future = asyncio.Future()


async def coro1():
    print("wait 1 second")
    await asyncio.sleep(1)
    print("set_result")
    future.set_result('data')


async def coro2():
    result = await future
    print(result)


loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait([
    coro1()
    coro2()
]))
loop.close()

輸出結果:

wait 1 second
(大約等待1秒)
set_result
data

這裏await後面跟隨的future對象,協程中yield from或者await後面可以調用future對象,其作用是:暫停協程,直到future執行結束或者返回result或拋出異常。

而在我們的例子中,await future必須要等待future.set_result('data')後才能夠結束。將coro2()作爲第二個協程可能體現得不夠明顯,可以將協程的調用改成這樣:

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait([
    # coro1(),
    coro2(),
    coro1()
]))
loop.close()

輸出的結果仍舊與上面相同。

其實,async這個關鍵字的用法不止能用在函數上,還有async with異步上下文管理器,async for異步迭代器. 對這些感興趣且覺得有用的可以網上找找資料,這裏限於篇幅就不過多展開了。

總結

本文就生成器和協程做了一些學習、探究和總結,不過並沒有做過多深入深入的研究。權且作爲入門到一個筆記,之後將會嘗試自己實現一下異步API,希望有助於理解學習。

參考鏈接

Python協程 https://thief.one/2017/02/20/Python%E5%8D%8F%E7%A8%8B/

http://www.dabeaz.com/coroutines/Coroutines.pdf

Coroutines

How the heck does async/await work in Python 3.5

Python3.4協程文檔

Python3.5協程文檔

廖雪峯的Python教程--協程

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