Python协程

当一个大型任务由多个小任务组成时,为了让任务在执行时更有效率,避免无谓的等待,我们总是让单位时间内能有更多的任务被执行,以期更早的完成全部任务。经常使用的方式包括多进程并行,单进程多线程并发和单进程单线程的异步。本文讨论的就是第三种方式。

所有的任务以异步的形式在单进程单线程中执行,多任务之间的协调与调度交由事件循环(Event Loop)控制,我们将这样的并发任务处置方式也成为协程(Coroutines)。

Python从3.4版本引入了asyncio模块是异步编程的主要模块,该模块在3.5版本时加入了新的关键字,让任务的协调更加便捷。

我们从简单的例子入手了解协程的运行机制:

import asyncio


@asyncio.coroutine
def my_coroutine(*args):
    result = 0
    for x in args:
        result += x
    return result


coro1 = my_coroutine(*[1, 3, 5, 7, 9])
print(coro1)
loop = asyncio.get_event_loop()
result = loop.run_until_complete(coro1)
print(result)

运行结果为:

<generator object my_coroutine at 0x000001EEA991DA20>
25

通过使用@asyncio.coroutine修饰的my_coroutine不再是一个普通的函数了,当“执行”my_coroutine(*args)的时候,不是获得函数的返回值而是获得一个协程对象。loop是一个事件循环,负责执行和调度协程。此时事件循环尚未启动,通过run_until_complete函数的名称就可以知道,该函数负责启动事件循环,执行通过参数传入的协程任务,当协程都执行完毕后事件循环也就停止了运行。事件循环停止运行时可以获得协程的运行结果。

可以向事件循环中挂载多个协程:

import asyncio


@asyncio.coroutine
def my_coroutine(*args):
    result = 0
    for x in args:
        result += x
    return result


coro1 = my_coroutine(*[1, 3, 5, 7, 9])
coro2 = my_coroutine(*[2, 4, 6, 8, 10])
coro3 = my_coroutine(*[1, 2, 3, 4, 5])
print(coro1)
loop = asyncio.get_event_loop()
result = loop.run_until_complete(asyncio.gather(coro1,coro2,coro3))
print(result)

通过asyncio.gather向事件循环中挂载了三个协程coro1,coro2,coro3,运行结果为:

<generator object my_coroutine at 0x000002155E214930>
[25, 30, 15]

运行结果为一个列表,注意列表中结果的顺序与协程挂载时的顺序是一样的。

另外还可以使用asyncio.wait来挂载多个协程:

import asyncio


@asyncio.coroutine
def my_coroutine(*args):
    result = 0
    for x in args:
        result += x
    return result


coro1 = my_coroutine(*[1, 3, 5, 7, 9])
coro2 = my_coroutine(*[2, 4, 6, 8, 10])
coro3 = my_coroutine(*[1, 2, 3, 4, 5])
print(coro1)
loop = asyncio.get_event_loop()
result = loop.run_until_complete(asyncio.wait([coro1,coro2,coro3]))
print(result)

运行结果为:

<generator object my_coroutine at 0x0000027E998C4930>
({<Task finished coro=<my_coroutine() done, defined at c:\...\python\python37\Lib\asyncio\coroutines.py:118> result=30>, <Task finished coro=<my_coroutine() done, defined at c:\...\python\python37\Lib\asyncio\coroutines.py:118> result=25>, <Task finished coro=<my_coroutine() done, defined at c:\...\python\python37\Lib\asyncio\coroutines.py:118> result=15>}, set())

这里有几点需要注意:

1. asyncio.wait要求提供列表形式的参数

2. result并不是协程运行的结果,而是一个元组,元组的第一个元素是一个集合(set),里面存放了三个Task对象。

3. 每个Task的对象代表一个协程,但是集合是无序的,这也就说明集合中Task的顺序与协程挂载时的顺序是不一样的。

Task和Future

Future是一个用来获得协程状态的类。一个Future对象中包含了协程的状态(正在执行,执行完成,取消),协程的结果或是协程的异常。而Task是Future的子类,从上面的运行结果来看,第一个Task对象对应的协程状态为执行完成(finished),运行结果为30(result = 30)。

实际上,每当一个协程在 事件循环中准备被执行时,协程会先被包装为一个Task对象。当然你也可以显示的将一个协程包装为Future对象或者Task对象后再放入事件循环中。

import asyncio


@asyncio.coroutine
def my_coroutine(*args):
    result = 0
    for x in args:
        result += x
    return result


coro1 = my_coroutine(*[1, 3, 5, 7, 9])
coro2 = my_coroutine(*[2, 4, 6, 8, 10])
coro3 = my_coroutine(*[1, 2, 3, 4, 5])
loop = asyncio.get_event_loop()
task1 = loop.create_task(coro1)
task2 = loop.create_task(coro2)
task3 = loop.create_task(coro3)
print(task1)
print(task1.done())
result = loop.run_until_complete(asyncio.wait([task1,task2,task3]))
print(result)
print(task1.done())
print(task1.result())

运行结果:

<Task pending coro=<my_coroutine() running at c:\...\python\python37\Lib\asyncio\coroutines.py:118>>
False
({<Task finished coro=<my_coroutine() done, defined at c:\...\python\python37\Lib\asyncio\coroutines.py:118> result=30>, <Task finished coro=<my_coroutine() done, defined at c:\...\python\python37\Lib\asyncio\coroutines.py:118> result=15>, <Task finished coro=<my_coroutine() done, defined at c:\...\python\python37\Lib\asyncio\coroutines.py:118> result=25>}, set())
True
25

通过调用事件循环的create_task函数将一个协程显式的包装为了一个Task对象。在将task1通过asyncio.wait挂载到事件循环之前,可以看到task1的状态为挂起(pending),协程的状态为正在运行(running),此时询问task1是否完成得到的是False。当事件循环结束后,再次询问task1是否完成得到的是True,并且可以通过Task的result函数获得该Task对象对应的协程执行结束后的结果。

注意,如果task1尚未完成,调用result函数会产生异常,因此此时无法获得对应协程的执行结果。

协程的嵌套

在一个协程内部使用yield from关键字调起另外一个协程,就称为协程的嵌套。我们知道,协程的执行必须在事件循环中,同一时刻只能有一个协程处于执行状态,那么当一个协程使用yield from调起另外一个协程的时候,调起方将主动放弃当前在事件循环中的运行状态而挂起,被调起方将在事件循环中获得执行,当被调起方执行结束后,调起方将获得在事件循环中继续执行的机会。

import asyncio


@asyncio.coroutine
def my_coroutine(*args):
    print('my_coroutine is running...')
    result = 0
    for x in args:
        result += x
    print('my_coroutine is finished...')
    return result


@asyncio.coroutine
def my_power(*arg, p):
    print('my_power is running...')
    num = yield from my_coroutine(*arg)
    print('my_power is finished...')
    return pow(num, p)


loop = asyncio.get_event_loop()
task1 = loop.create_task(my_power(p=3, *[1, 3, 5]))
print(task1)
print(task1.done())
result = loop.run_until_complete(task1)
print(result)
print(task1.done())
print(task1.result())

运行结果:

<Task pending coro=<my_power() running at D:/PycharmProjects/MyTest/mycoroutines2.py:14>>
False
my_power is running...
my_coroutine is running...
my_coroutine is finished...
my_power is finished...
729
True
729

本例中有两个协程,my_power()和my_coroutine(),在my_power()内部通过yield from调起my_coroutine(),调起的目的是为了获得my_coroutine()执行的结果,并作为my_power()下一步计算的依据。

使用yield from后,my_power()主动放弃了事件循环中的运行将自己挂起,事件循环开始执行my_coroutine(),执行完毕后将事件循环继续执行my_power(),此时的my_power()拥有了my_coroutine()执行的结果。

回调

如果从解决“当my_coroutine()执行结束后,根据执行结果计算幂”这个问题的角度出发,还可以采用回调机制,即为协程my_coroutine()添加一个执行完成后再开始执行的函数。

import asyncio
from functools import partial


@asyncio.coroutine
def my_coroutine(*args):
    print('my_coroutine is running...')
    result = 0
    for x in args:
        result += x
    print('my_coroutine is finished...')
    return result


def my_power(p, task):
    print('my_power is running...')
    result = pow(task.result(), p)
    print('my_power is finished...')
    print(result)


loop = asyncio.get_event_loop()
task1 = loop.create_task(my_coroutine(*[1, 3, 5]))
task1.add_done_callback(partial(my_power, 3))
loop.run_until_complete(task1)

运行结果:

my_coroutine is running...
my_coroutine is finished...
my_power is running...
my_power is finished...
729

通过task1的add_done_callback函数可以在task1执行结束后调用以参数形式传入的函数。

作为回调函数需要有几点注意:

1. 回调函数是一个普通函数,不能将协程作为回调发送。

2. 在回调函数被调用的时候,已完成的Task对象会以最后一个参数的形式传入到回调函数中。

3.在add_done_callback中注册回调函数时,只能提供函数名称。所以如果回调函数中有多个参数时,需要以偏函数的形式提供那些参数的值。回调函数的最后一个参数是完成的Task对象本身,由Task对象处理。

async和wait关键字

在python 3.5版本中,为asyncio模块增加了async和await两个新的关键字。他们从语义上与前面的@async.coroutine和yield from一样。但是必须注意的是,尽管语义相同,但是不能混用,也就是说yield from必须用在@async.coroutine定义的协程中,而await必须用在async定义的协程中。

import asyncio


async def my_coroutine(*args):
    print('my_coroutine is running...')
    result = 0
    for x in args:
        result += x
    print('my_coroutine is finished...')
    return result


async def my_power(*args, p):
    print('my_power is running...')
    num = await my_coroutine(*args)
    print('my_power is finished...')
    return pow(num, p)


loop = asyncio.get_event_loop()
task1 = loop.create_task(my_power(p=3, *[1, 3, 5]))
result = loop.run_until_complete(task1)
print(task1)
print(task1.done())
print(result)
print(task1.done())
print(task1.result())

运行结果:

<Task pending coro=<my_power() running at D:/PycharmProjects/MyTest/mycoroutines2.py:13>>
False
my_power is running...
my_coroutine is running...
my_coroutine is finished...
my_power is finished...
729
True
729

利用async和await关键字改写了之前@async.coroutine和yield from的例子,运行结果是一样的。

最后我们再看一个案例并分析运行结果,巩固协程调度知识的总结:

import asyncio
import random
import time


async def waiter(name):
    count = 0
    for x in range(4):
        t = random.randint(1, 3)/4
        count += t
        print('%s is preparing sleep %.2f' % (name, t))
        time.sleep(t)
        print('%s sleep %.2f sec. and wake up' % (name, t))
    print('%s is finish, total sleep %.2f seconds' % (name, count))


async def main():
    start = time.time()
    print('main is preparing...')
    asyncio.wait([waiter('FOO'), waiter('BAR'), waiter('QUX')])
    print('main is finish! use %f seconds' % (time.time()-start))


loop = asyncio.get_event_loop()
task = loop.create_task(main())
loop.run_until_complete(task)

代码中定义了两个协程waiter()和main(),在main()中创建了多个任务。挂载时将main()挂载到了事件循环,此时代码运行会是什么结果呢?

main is preparing...
D:/PycharmProjects/MyTest/my20181031_7.py:22: RuntimeWarning: coroutine 'wait' was never awaited
  asyncio.wait([waiter('FOO'), waiter('BAR'), waiter('QUX')])
D:/PycharmProjects/MyTest/my20181031_7.py:22: RuntimeWarning: coroutine 'waiter' was never awaited
  asyncio.wait([waiter('FOO'), waiter('BAR'), waiter('QUX')])
main is finish! use 0.008976 seconds

此时对于事件循环来说,实际只有一个任务main()需要运行,而协程main()中四行代码瞬间就会执行完毕。注意第三行虽然创建了一个新的任务集合,但是协程main()并没有放弃自己在事件循环中的运行,一口气将自己运行完毕,随着任务main()的运行结束事件循环停止。因此在main()中创建的任务集合也没法运行了。

所以此时应该修改协程main(),在第三行加上await关键字,创建任务集合后main()放弃在事件循环中的运行将自己挂起,等待任务集合在事件循环中都完成后才会继续在事件循环中运行自己。

import asyncio
import random
import time


async def waiter(name):
    count = 0
    for x in range(4):
        t = random.randint(1, 3)/4
        count += t
        print('%s is preparing sleep %.2f' % (name, t))
        time.sleep(t)
        print('%s sleep %.2f sec. and wake up' % (name, t))
    print('%s is finish, total sleep %.2f seconds' % (name, count))


async def main():
    start = time.time()
    print('main is preparing...')
    await asyncio.wait([waiter('FOO'), waiter('BAR'), waiter('QUX')])
    print('main is finish! use %f seconds' % (time.time()-start))


loop = asyncio.get_event_loop()
task = loop.create_task(main())
loop.run_until_complete(task)

我们看看此时的运行结果:

main is preparing...
QUX is preparing sleep 0.25
QUX sleep 0.25 sec. and wake up
QUX is preparing sleep 0.75
QUX sleep 0.75 sec. and wake up
QUX is preparing sleep 0.50
QUX sleep 0.50 sec. and wake up
QUX is preparing sleep 0.50
QUX sleep 0.50 sec. and wake up
QUX is finish, total sleep 2.00 seconds
BAR is preparing sleep 0.25
BAR sleep 0.25 sec. and wake up
BAR is preparing sleep 0.50
BAR sleep 0.50 sec. and wake up
BAR is preparing sleep 0.75
BAR sleep 0.75 sec. and wake up
BAR is preparing sleep 0.75
BAR sleep 0.75 sec. and wake up
BAR is finish, total sleep 2.25 seconds
FOO is preparing sleep 0.75
FOO sleep 0.75 sec. and wake up
FOO is preparing sleep 0.25
FOO sleep 0.25 sec. and wake up
FOO is preparing sleep 0.50
FOO sleep 0.50 sec. and wake up
FOO is preparing sleep 0.25
FOO sleep 0.25 sec. and wake up
FOO is finish, total sleep 1.75 seconds
main is finish! use 6.006099 seconds

加上了await关键字后,main()协程确实做了一次"耐心"的等待,任务集合中先执行了名为QUX的任务,然后时BAR最后是FOO,无论是哪个任务,中间都有大段的sleep时间,为了提高效率,应该在任务进入sleep时将自己挂起,让事件循环去执行调起的协程。所以尝试着再waiter中的sleep前加上await:

import asyncio
import random
import time


async def waiter(name):
    count = 0
    for x in range(4):
        t = random.randint(1, 3)/4
        count += t
        print('%s is preparing sleep %.2f' % (name, t))
        await time.sleep(t)
        print('%s sleep %.2f sec. and wake up' % (name, t))
    print('%s is finish, total sleep %.2f seconds' % (name, count))


async def main():
    start = time.time()
    print('main is preparing...')
    await asyncio.wait([waiter('FOO'), waiter('BAR'), waiter('QUX')])
    print('main is finish! use %f seconds' % (time.time()-start))


loop = asyncio.get_event_loop()
task = loop.create_task(main())
loop.run_until_complete(task)

此时看运行结果:

main is preparing...
QUX is preparing sleep 0.75
BAR is preparing sleep 0.50
FOO is preparing sleep 0.25
main is finish! use 1.503001 seconds
Task exception was never retrieved
future: <Task finished coro=<waiter() done, defined at D:/PycharmProjects/MyTest/my20181031_7.py:6> exception=TypeError("object NoneType can't be used in 'await' expression")>
Traceback (most recent call last):
  File "D:/PycharmProjects/MyTest/my20181031_7.py", line 12, in waiter
    await time.sleep(t)
TypeError: object NoneType can't be used in 'await' expression
Task exception was never retrieved
future: <Task finished coro=<waiter() done, defined at D:/PycharmProjects/MyTest/my20181031_7.py:6> exception=TypeError("object NoneType can't be used in 'await' expression")>
Traceback (most recent call last):
  File "D:/PycharmProjects/MyTest/my20181031_7.py", line 12, in waiter
    await time.sleep(t)
TypeError: object NoneType can't be used in 'await' expression
Task exception was never retrieved
future: <Task finished coro=<waiter() done, defined at D:/PycharmProjects/MyTest/my20181031_7.py:6> exception=TypeError("object NoneType can't be used in 'await' expression")>
Traceback (most recent call last):
  File "D:/PycharmProjects/MyTest/my20181031_7.py", line 12, in waiter
    await time.sleep(t)
TypeError: object NoneType can't be used in 'await' expression

首先看到的QUX任务进入睡眠后确实挂起了自己,事件循环开始执行BAR任务,当BAR任务进行睡眠后将自己挂起,事件循环开始执行FOO任务,但是当FOO任务挂起后,main()任务获得了执行,当main()执行结束后事件循环结束。

为什么main()获得了执行,这是因为当QUX,BAR和FOO三个任务都已异常的方式结束了自己的运行(状态时done,exception=TypeError),所以此时任务集合执行结束,main()获得执行机会。

为什么QUX,BAR和FOO三个任务都产生了异常?因为await(包括yield from)后面只能跟另一个协程或任务,而time.sleep()时一个普通的函数,所以产生了错误。修改的方式就是将waiter中await后面的内容改为协程或任务:

import asyncio
import random
import time


async def sleep(sec):
    asyncio.sleep(sec)


async def waiter(name):
    count = 0
    for x in range(4):
        t = random.randint(1, 3)/4
        count += t
        print('%s is preparing sleep %.2f' % (name, t))
        await asyncio.sleep(t)
        print('%s sleep %.2f sec. and wake up' % (name, t))
    print('%s is finish, total sleep %.2f seconds' % (name, count))


async def main():
    start = time.time()
    print('main is preparing...')
    await asyncio.wait([waiter('FOO'), waiter('BAR'), waiter('QUX')])
    print('main is finish! use %f seconds' % (time.time()-start))


loop = asyncio.get_event_loop()
task = loop.create_task(main())
loop.run_until_complete(task)

这里将time.sleep()函数改为了asyncio.sleep()协程,我们看一下asyncio.sleep()的关键源代码:

async def sleep(delay, result=None, *, loop=None):
    """Coroutine that completes after a given time (in seconds)."""
    ... # 省略部分代码
    if loop is None:
        loop = events.get_event_loop()
    future = loop.create_future()
    h = loop.call_later(delay,
                        futures._set_result_unless_cancelled,
                        future, result)
    try:
        return await future
    finally:
        h.cancel()

可以看到asyncio.sleep()是一个协程,在这个协程内部它也要await另一个指定时间后执行的任务,等待期间sleep()也会将自己挂起,事件循环就会去执行其他的等待执行的任务。

所以修改之后的运行结果是:

main is preparing...
QUX is preparing sleep 0.50
BAR is preparing sleep 0.50
FOO is preparing sleep 0.75
QUX sleep 0.50 sec. and wake up
QUX is preparing sleep 0.50
BAR sleep 0.50 sec. and wake up
BAR is preparing sleep 0.75
FOO sleep 0.75 sec. and wake up
FOO is preparing sleep 0.50
QUX sleep 0.50 sec. and wake up
QUX is preparing sleep 0.25
BAR sleep 0.75 sec. and wake up
BAR is preparing sleep 0.75
FOO sleep 0.50 sec. and wake up
FOO is preparing sleep 0.25
QUX sleep 0.25 sec. and wake up
QUX is preparing sleep 0.25
FOO sleep 0.25 sec. and wake up
FOO is preparing sleep 0.25
QUX sleep 0.25 sec. and wake up
QUX is finish, total sleep 1.50 seconds
FOO sleep 0.25 sec. and wake up
FOO is finish, total sleep 1.75 seconds
BAR sleep 0.75 sec. and wake up
BAR is preparing sleep 0.25
BAR sleep 0.25 sec. and wake up
BAR is finish, total sleep 2.25 seconds
main is finish! use 2.252533 seconds

QUX进入等待的0.5秒,BAR获得执行但随即马上进入0.5秒等待,FOO获得执行随即马上进入0.75秒等待。QUX的等待结束后重新开始执行随即进入下一个0.5秒等待,BAR等待结束后获得执行但随即马上进入0.75秒等待,FOO等待获得执行随即马上进入0.5秒等待,此时任务调度得当。任务全部结束时总耗时与QUX,BAR,FOO最长耗时的那个任务时间基本一致。这就是并发执行的效果。

协程(Coroutine)是一种用户态的轻量级线程,它可以在单个线程中实现多任务并发处理。Python中的协程通过生成器(generator)实现,使用yield语句来实现协程的暂停和恢复操作。 在Python 3.5之后,Python引入了async/await关键字,使得协程的使用更加方便和简洁。 下面是一个使用yield实现协程的示例: ```python def coroutine(): print("coroutine started") while True: value = yield print("Received value: ", value) c = coroutine() next(c) # 启动协程 c.send(1) # 发送值,并打印接收到的值 c.send(2) ``` 输出: ``` coroutine started Received value: 1 Received value: 2 ``` 在上面的代码中,使用yield语句实现了协程的暂停和恢复操作。在调用`c.send()`方法时,会将值发送给协程,并从yield语句处恢复协程的执行。协程会处理接收到的值,并在下一个yield语句处暂停,等待下一次发送。 除了使用yield语句来实现协程外,Python 3.5之后还可以使用async/await关键字来定义协程。使用async/await关键字定义的协程更加简洁和易于理解。下面是一个使用async/await关键字实现的协程示例: ```python async def coroutine(): print("coroutine started") while True: value = await asyncio.sleep(1) print("Received value: ", value) asyncio.run(coroutine()) ``` 在上面的代码中,使用async/await关键字定义了一个协程。使用`asyncio.sleep()`函数来实现协程的暂停操作,并在下一次事件循环时恢复协程的执行。使用`asyncio.run()`函数来运行协程。 总的来说,协程是一种非常有用的并发编程技术,可以在单个线程中实现高并发的处理。在Python中,可以使用生成器和async/await关键字来实现协程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值