【2019.05.30】python教程系列 - 协程入门教程(异步IO)Async

此文使用 python3.7,用async 声明协程函数。
在实际过程中,什么功能的函数要用async声明为协程函数呢?就是那些能发挥异步IO性能的函数,比如读写文件、读写网络、读写数据库,这些都是浪费时间的IO操作,把它们协程化、异步化从而提高程序的整体效率(速度)。

1 创建一个协程

Python 3.7 推荐使用 async/await 语法来声明协程,来编写异步应用程序。我们来创建一个协程函数:
首先打印一行“你好”,等待1秒钟后再打印“学习协程”。

import asyncio
async def say_delay():
    print('你好')
    await asyncio.sleep(1)
    print('学习协程')
asyncio.run(say_delay())

在这里插入图片描述

  • 如何真正运行一个协程呢?asyncio 提供了三种机制:

(1)asyncio.run() 函数,这是异步程序的主入口,相当于C语言中的main函数。

(2)用await等待协程,比如上例中的await asyncio.sleep(1)。再看下面的例子,我们定义了协程 say_delay() ,在main()协程中调用两次,第一次延迟1秒后打印“你好”,第二次延迟2秒后打印“学习协程”。这样我们通过 await 运行了两个协程。

import asyncio
import time


async def say_delay(msg, delay):
    await asyncio.sleep(delay)
    print(msg)


async def main():
    print(f"begin at {time.strftime('%H-%M-%S')}")
    await say_delay('你好', 1)
    await say_delay('学习协程', 2)
    print(f"end at {time.strftime('%H-%M-%S')}")


asyncio.run(main())

在这里插入图片描述
从起止时间可以看出,两个协程是顺序执行的,总共耗时1+2=3秒。
(3)通过asyncio.create_task()函数并发运行作为 asyncio 任务(Task) 的多个协程。下面,我们用create_task()来修改上面的main()协程,从而让两个say_delay()协程并发运行:

import asyncio
import time


async def say_delay(msg, delay):
    await asyncio.sleep(delay)
    print(msg)


async def main():
    task1 = asyncio.create_task(say_delay('你好', 1))
    task2 = asyncio.create_task(say_delay('学习协程', 2))
    print(f"begin at {time.strftime('%H-%M-%S')}")
    await task1
    await task2
    print(f"end at {time.strftime('%H-%M-%S')}")


asyncio.run(main())

在这里插入图片描述
从运行结果的起止时间可以看出,两个协程是并发执行的了,总耗时等于最大耗时2秒。

asyncio.create_task() 是一个很有用的函数,在爬虫中它可以帮助我们实现大量并发去下载网页。在Python 3.6中与它对应的是 ensure_future()

可等待对象(awaitables)

可等待对象,就是可以在 await 表达式中使用的对象,前面我们已经接触了两种可等待对象的类型:协程和任务,还有一个是低层级的Future。

asyncio模块的许多API都需要传入可等待对象,比如 run(), create_task() 等等。

(1)协程

协程是可等待对象,可以在其它协程中被等待。协程两个紧密相关的概念是:
  • 协程函数:通过 async def 定义的函数;
  • 协程对象:调用协程函数返回的对象。
import asyncio
import time


async def now_time():
    return time.time()


async def main():
    now1 = now_time()
    print('now1 is ', type(now1))

    now2 = await now1
    print(f'now is {now2}')

    now3 = await now_time()
    print(f'now is {now3}')


asyncio.run(main())

运行上面这段程序,结果为:
在这里插入图片描述
可以看到,直接运行协程函数 now_time()得到的now1是一个协程对象,因为协程对象是可等待的,所以通过 await 得到真正的当前时间。now3是直接await 协程函数,也得到了当前时间的返回值。

(2)任务

前面我们讲到,任务是用来调度协程的,以便并发执行协程。当一个协程通过 asyncio.create_task() 被打包为一个 任务,该协程将自动加入程序调度日程准备立即运行。

create_task()的基本使用前面例子已经讲过。它返回的task通过await来等待其运行完。如果,我们不等待,会发生什么?“准备立即运行”又该如何理解呢?先看看下面这个例子:

import asyncio
import time


async def whattime(i):
    await asyncio.sleep(1)
    print(f'calling: {i}, now is {time.strftime("%X")}')


async def main():
    task = asyncio.create_task(whattime(0))
    await task
    for i in range(1, 5):
        s = asyncio.create_task(whattime(i))

    await  asyncio.sleep(1)

asyncio.run(main())

运行这段代码的情况是这样的:

  1. 首先,1秒钟后打印一行,
  2. 接着,停顿1秒后,连续打印4行:

结果如下
在这里插入图片描述
从这个结果看,asyncio.create_task()产生的4个任务,我们并没有await,它们也执行了。关键在于这一行await asyncio.sleep(1)的 await,如果把这一行去掉或是sleep的时间小于1秒(比whattime()里面的sleep时间少即可),就会只看到第一行的输出结果而看不到后面四行的输出。这是因为,main()不sleep或sleep少于1秒钟,main()就在whattime()还未来得及打印结果(因为,它要sleep 1秒)就退出了,从而整个程序也退出了,就没有whattime()的输出结果。

再来理解一下“准备立即执行”这个说法。它的意思就是,create_task()只是打包了协程并加入调度队列还未执行,并准备立即执行,什么时候执行呢?在“主协程”(调用create_task()的协程)挂起的时候。

这里的 “挂起” 有两个方式:

1. 通过 await task 来执行这个任务;
2. 主协程通过 await sleep 挂起,事件循环就去执行task了。

我们知道,asyncio是通过事件循环实现异步的。在主协程 main()里面,没有遇到 await 时,事件就是执行main()函数,遇到 await 时,事件循环就去执行别的协程,即create_task()生成的whattime()的4个任务,这些任务一开始就是 await sleep 1秒。这时候,主协程和4个任务协程都挂起了,CPU空闲,事件循环等待协程的消息。

如果main()协程只sleep了0.1秒,它就先醒了,给事件循环发消息,事件循环就来继续执行main()协程,而main()后面已经没有代码,就退出该协程,退出它也就意味着整个程序退出,4个任务就没机会打印结果;

(3)Future

它是一个低层级的可等待对象,表示一个异步操作的最终结果。

总结

在编程过程中,我们可以把那些涉及耗费时间的IO操作(读写文件、数据库、网络)的函数通过 async def 异步化,就是异步编程。

那些异步函数(协程函数)都是通过消息机制被事件循环管理调度着,整个程序的执行是单线程的,但是某个协程A进行IO时,事件循环就去执行其它协程非IO的代码。当事件循环收到协程A结束IO的消息时,就又回来执行协程A,这样事件循环不断在协程之间转换,充分利用了IO的闲置时间,从而并发的进行多个IO操作,这就是异步IO。

写异步IO程序时记住一个准则:需要IO的地方异步。其它地方即使用了协程函数也是没用的。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值