Python 笔记(21)— 协程实现、链式调用、await作用、并发任务、协程调用普通函数

1. 什么是协程

Python 3.4 时候引进了协程的概念,它使用一种单线程单进程的的方式实现并发。

协程,英文名是 Coroutine, 又称为微线程,是一种用户态的轻量级线程。协程不像线程和进程那样,需要进行系统内核上的上下文切换,协程的上下文切换是由程序员决定的。

Python 中协程就是一个可以暂停执行的函数,听起来和生成器的概念一样。

Python3.4 开始协程被加入到标准库,当时的协程是通过 @asyncio.coroutineyeild from 实现的,看起来和生成器的实现方式没什么区别。后来为了更好的区分开协程和生成器,到 Python3.5 的时候引入 async/await 语法糖临时定格了下来,直到 Python3.6 的时候才被更多的人认可,后来到了 Python3.7 官方把 asyncawait 作为保留字,同时协程的调用也变得简单了许多。

2. 协程实现

2.1 原生实现

在同一个线程中,如果发生以下事情:

  • A 函数执行时被中断,传递一些数据给 B 函数;
  • B 函数拿到这些数据后开始执行,执行一段时间后,发送一些数据到 A 函数;

就这样交替执行……这种执行调用模式,被称为协程。

可以看到,协程是在同一线程中函数间的切换,而不是线程间的切换,因此执行效率更优,Python 的异步操作正是基于高效的协程机制。

下面通过一个例子,加深对协程的理解。

def A():
    a_list = ['1', '2', '3']
    for to_b in a_list:
        from_b = yield to_b
        print('receive %s from B' % (from_b,))
        print('do some complex process for A during 200ms ')

def B(a):
    from_a = a.send(None)
    print('response %s from A ' % (from_a,))
    print('B is analysising data from A')
    b_list = ['x', 'y', 'z']
    try:
        for to_a in b_list:
            from_a = a.send(to_a)
            print('response %s from A ' % (from_a,))
            print('B is analysising data from A')
    except StopIteration:
        print('---from a done---')
    finally:
        a.close()

if __name__ == "__main__":
	a = A()
	B(a)

分析执行过程:

  1. a.send(None) 激活 A 函数,并执行到 yield to_b ,把变量 to_b 传递给 B 函数,A 函数中断;

  2. from_a 就是 上步 A 函数返回的 to_b 值,然后执行分析这个值;

  3. 当执行到 a.send(to_a) 时, B 函数将加工后的 to_a 值发送给 A 函数;

  4. from_b 变量接收来自 B 函数的发送,然后使用此值做分析 200 ms 后,又将 to_b 传递给 B 函数,A 函数中断;

  5. 重复 2、 3、4

  6. 直到 from_a 获取不到响应值,函数触发 StopIteration 异常,程序执行结束。

执行结果:

response 1 from A
B is analysising data from A
receive x from B
do some complex process for A during 200ms
response 2 from A
B is analysising data from A
receive y from B
do some complex process for A during 200ms
response 3 from A
B is analysising data from A
receive z from B
do some complex process for A during 200ms
---from A done---

通过上述看到,协程是在同一个线程中,不同函数间交替的、协作的执行完成任务。

2.2 使用库函数

使用协程也就意味着你需要一直写异步方法。在 Python 中我们使用 asyncio 模块来实现一个协程。如果我们把 Python 中普通函数称之为同步函数(方法),那么用协程定义的函数我们称之为异步函数(方法)。

同步函数定义:

def regular_double(x):
    return 2 * x

异步函数定义:

async def async_double(x):
    return 2 * x

对于同步函数我们知道是这样调用的:

 regular_double(2)

异步函数如何调用呢?带着这个问题我们看下面的一个简单例子。

import asyncio

async def foo():
    print("这是一个协程")


if __name__ == '__main__':
    loop = asyncio.get_event_loop() # 定义一个事件循环
    try:
        print("开始运行协程")
        coro = foo()
        print("进入事件循环")
        loop.run_until_complete(coro) # 运行协程
    finally:
        print("关闭事件循环")
        loop.close() # 运行完关闭协程

输出结果:

开始运行协程
进入事件循环
这是一个协程
关闭事件循环

首先,需要导入需要的包 asyncio。然后,创建一个事件循环,因为协程是基于事件循环的。 之后,通过 run_until_complete 方法传入一个异步函数,来运行这个协程。 最后在结束的时候调用 close 方法关闭协程。 综上就是调用一个协程的写法。除此之外协程还有其他的不同之处。

3. 协程之间的链式调用

我们可以通过使用 await 关键字,在一个协程中调用一个协程。 一个协程可以启动另一个协程,从而可以使任务根据工作内容,封装到不同的协程中。我们可以在协程中使用 await 关键字,链式地调度协程,来形成一个协程任务流。像下面的例子一样:

import asyncio


async def main():
    print("主协程")
    print("等待result1协程运行")
    res1 = await result1()
    print("等待result2协程运行")
    res2 = await result2(res1)
    return (res1, res2)


async def result1():
    print("这是result1协程")
    return "result1"


async def result2(arg):
    print("这是result2协程")
    return f"result2接收了一个参数,{arg}"


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    try:
        result = loop.run_until_complete(main())
        print(f"获取返回值:{result}")
    finally:
        print("关闭事件循环")
        loop.close()

输出结果:

主协程
等待result1协程运行
这是result1协程
等待result2协程运行
这是result2协程
获取返回值:('result1', 'result2接收了一个参数,result1')
关闭事件循环

在上面,我们知道调用协程需要通过创建一个事件循环然后再去运行。 这里我们需要了解的是如果在协程里想调用一个协程我们需要使用 await 关键字,就拿上面的例子来说在 main 函数里调用协程 result1result2。 那么问题来了:await 干了什么呢?

4. await 的作用

关键字 await,它要用于这个 async 的函数内部,await 用于需要等待的地方(比如网络 IO),出现 await 的时候,asyncio 去调度,在不同的 task 里面切换。

await 的作用就是等待当前的协程运行结束之后再继续进行下面代码。因为我们执行 result1 的时间很短,所以在表面上看 result1result2 是一起执行的。这就是 await 的作用。

等待一个协程的执行完毕,如果有返回结果,那么就会接收到协程的返回结果,通过使用 return 可以返回协程的一个结果,这个和同步函数的 return 使用方法一样。

5. 并发执行任务

一系列的协程可以通过 await 链式调用,但是有的时候我们需要在一个协程里等待多个协程,比如我们在一个协程里等待 1000 个异步网络请求,对于访问次序没有要求的时候,就可以使用关键字 await 来解决了。await 可以暂停一个协程,直到后台操作完成。

import asyncio


async def num(n):
    print(f"当前的数字是:{n}")
    await asyncio.sleep(n)
    print(f"等待时间:{n}")


async def main():
    tasks = [num(i) for i in range(5)] #协程列表
    #await asyncio.gather(*tasks) #有序并发
    await asyncio.wait(tasks) #并发运行协程列表的协程


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.close()

输出结果:

当前的数字是:1
当前的数字是:2
当前的数字是:3
当前的数字是:0
当前的数字是:4
等待时间:0
等待时间:1
等待时间:2
等待时间:3
等待时间:4

如果运行的话会发现首先会打印 5 次数字,但是并不是顺序执行的,这也说明 asyncio.wait 并发执行的时候是乱序的。如果想保证顺序只要使用 gathertask 写成解包的形式就行了,也就是上面的注释部分的代码(实际测试并不能保证顺序执行)。

6. 在协程中使用普通的函数

在协程中可以通过一些方法去调用普通的函数。可以使用的关键字有 call_soon 等。可以通过字面意思理解调用立即返回。 下面来看一下具体的使用例子:

import asyncio
import functools


def callback(args, *, kwargs="defalut"):
    print(f"普通函数做为回调函数,获取参数:{args}{kwargs}")


async def main(loop):
    print("注册callback")
    loop.call_soon(callback, 1)
    wrapped = functools.partial(callback, kwargs="not defalut")
    loop.call_soon(wrapped, 2)
    await asyncio.sleep(0.2)


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
try:
    loop.run_until_complete(main(loop))
finally:
    loop.close()

输出结果:

注册callback
普通函数做为回调函数,获取参数:1,defalut
普通函数做为回调函数,获取参数:2,not defalut

7. 协程优点

多线程编程是比较困难的, 因为调度程序任何时候都能中断线程, 必须记住保留锁, 去保护程序中重要部分, 防止多线程在执行的过程中断。

而协程默认会做好全方位保护, 以防止中断。我们必须显示产出才能让程序的余下部分运行。对协程来说, 无需保留锁, 而在多个线程之间同步操作, 协程自身就会同步, 因为在任意时刻, 只有一个协程运行。

总结下大概下面几点:

  • 无需系统内核的上下文切换,减小开销;
  • 无需原子操作锁定及同步的开销,不用担心资源共享的问题;
  • 单线程即可实现高并发,单核 CPU 即便支持上万的协程都不是问题,所以很适合用于高并发处理,尤其是在应用在网络爬虫中。

8. 协程缺点

  • 无法使用 CPU 的多核

协程的本质是个单线程,它不能同时用上单个 CPU 的多个核,协程需要和进程配合才能运行在多 CPU 上。当然我们日常所编写的绝大部分应用都没有这个必要,就比如网络爬虫来说,限制爬虫的速度还有其他的因素,比如网站并发量、网速等问题都会是爬虫速度限制的因素。除非做一些密集型应用,这个时候才可能会用到多进程和协程。

  • 处处都要使用非阻塞代码

写协程就意味着你要一值写一些非阻塞的代码,使用各种异步版本的库, 不过这些缺点并不能影响到使用协程的优势。

协程的并发是异步 IO 方式实现的,其实在 Python 3.4 的时候使用 Yeild 的方式实现的,只是后来加入了语法糖 async/awati,协程遇到 IO 操作就切换,协程之所以能处理大并发,就是由于挤掉了 IO 操作,使得 CPU 一直运行。 事件循环和异步是两个概念,事件循环是执行我们的异步代码并决定如何在异步函数之间切换的对象。如果某个协程在等待某些资源,我们需要暂停它的执行,在事件循环中注册这个事件,以便当事件发生的时候,能再次唤醒该协程的执行。 运行异步函数我们首先需要创建一个协程,然后创建 Future 或 Task 对象,将它们添加到事件循环中,也就是说异步函数是需要靠事件循环运行的。协程本质上是单线程的,那这样的异步方式,是通过异步函数之间切换的实现的,因为没有阻塞,所以速度很多,看起来像是并发。实际上并不是并发。

原文链接:
https://docs.python.org/zh-cn/3.7/library/index.html
https://gitbook.cn/books/5cda6064e8757e4d2f2fd292/index.html

https://gitbook.cn/gitchat/activity/5e3c11c1f55e4f503f2611f0

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值