理解Python的协程(Coroutine)

因为GIL的存在,致使Python多线程性能甚至比单线程更糟。html

GIL: 全局解释器锁(英语:Global Interpreter Lock,缩写GIL),是计算机程序设计语言解释器用于同步线程的一种机制,它使得任什么时候刻仅有一个线程在执行。[1]即使在多核心处理器上,使用 GIL 的解释器也只容许同一时间执行一个线程。python

因而出现了协程(Coroutine)这么个东西。git

协程: 协程,又称微线程,纤程,英文名Coroutine。协程的做用,是在执行函数A时,能够随时中断,去执行函数B,而后中断继续执行函数A(能够自由切换)。但这一过程并非函数调用(没有调用语句),这一整个过程看似像多线程,然而协程只有一个线程执行.express

协程因为由程序主动控制切换,没有线程切换的开销,因此执行效率极高。对于IO密集型任务很是适用,若是是cpu密集型,推荐多进程+协程的方式。多线程

在Python3.4以前,官方没有对协程的支持,存在一些三方库的实现,好比gevent和Tornado。3.4以后就内置了asyncio标准库,官方真正实现了协程这一特性。app

而Python对协程的支持,是经过Generator实现的,协程是遵循某些规则的生成器。所以,咱们在了解协程以前,咱们先要学习生成器。异步

生成器(Generator)

咱们这里主要讨论yieldyield from这两个表达式,这两个表达式和协程的实现息息相关。async

  • Python2.5中引入yield表达式,参见PEP342
  • Python3.3中增长yield from语法,参见PEP380

方法中包含yield表达式后,Python会将其视做generator对象,再也不是普通的方法。ide

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点调用流程以下图:

tulip_coro.png

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/

www.dabeaz.com/coroutines/…

Coroutines

How the heck does async/await work in Python 3.5

Python3.4协程文档

Python3.5协程文档

廖雪峰的Python教程--协程