初识 Python asyncio

1. 前言

引用廖雪峰老师的话

由于我们要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配,多线程和多进程只是解决这一问题的一种方法。

另一种解决IO问题的方法是异步IO。当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。

这就是为什么需要异步IO。在一个线程中,在多个协程之间切换,充分实现CPU的高效使用。

先讲讲同步/异步和阻塞/非阻塞:建议看 知乎 怎样理解阻塞非阻塞与同步异步的区别? YI LU的回答

同步和异步关注的是消息通信机制;阻塞和非阻塞关注的是程序在等待调用结果时的状态。

  • 同步。就是在发出一个调用时,没得到结果之前,该调用就不返回。但是一旦调用返回就得到返回值了,调用者主动等待这个调用的结果
  • 异步。就是在发出一个调用时,这个调用就直接返回了,不管返回有没有结果。当一个异步过程调用发出后,被调用者通过状态,通知来通知调用者,或者通过回调函数处理这个调用
    -阻塞。调用结果返回之前,当前线程会被“挂起”,直到结果返回,才会恢复执行。
  • 非阻塞。调用结果返回之前,当前线程可以去做其他事情,但是要时不时地检查是否有结果返回了。

网络上的例子

老张爱喝茶,废话不说,煮开水。
出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
1 老张把水壶放到火上,立等水开。(同步阻塞);立等就是阻塞了老张去干别的事,老张得一直主动的看着水开没,这就是同步

2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞);老张去看电视了,这就是非阻塞了,但是老张还是得关注着水开没,这也就是同步了

3 老张把响水壶放到火上,立等水开。(异步阻塞);立等就是阻塞了老张去干别的事,但是老张不用时刻关注水开没,因为水开了,响水壶会提醒他,这就是异步了

4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞);老张去看电视了,这就是非阻塞了,而且,等水开了,响水壶会提醒他,这就是异步

举个例子,看看之前的同步编程。

do_something()
f = open('test.txt')
result = f.read() # 线程停在此处,等待IO的结果,这段时间CPU啥事也没干,浪费了CPU资源
do_othner_something()
f.close()

而使用异步编程,就是当CPU空闲的时候,可以转而去做其他事情,这岂不美哉。

就好比拿水桶去接水。

同步编程:把桶放在水龙头下边,打开水龙头。这期间你就等着水接满,啥也不干。
异步编程:把桶放在水龙头下边,打开水龙头。不用等水接满,水满了自然会有动静告诉你已经满了,这期间你可以接第二桶水、第三桶水。

2. 核心概念

2.1 coroutine(协程)

协程,也叫做微线程。在一个线程之间,是一种用户态上下文切换技术,实现CPU的高效使用。在协程获得返回值之前可以暂停执行,将执行权转交给其他协程一段时间。

协程函数:用async def定义的函数。
协程对象:

协程的实现:

  • yield/yield from语法。
  • asyncio.coroutine装饰器。
  • async/await语法。推荐使用。
    如下代码所示,定义了一个协程函数 main
>>> import asyncio

>>> async def main():
...     print('hello')
...     await asyncio.sleep(1)
...     print('world')

>>> asyncio.run(main())
hello
world

当使用asycio.run(main())启动协程时,会发现打印hello之后隔1s会打印world。当然,现在看来这个同步的代码是一样(直接使用time.sleep(1)不也是一样的效果么?)。这是因为目前,没有多余的协程可以切换,所以看不出效果。

注意,当我们使用main()时,并没有执行协程。调用协程函数只是得到了一个协程对象(),单并不会执行这个协程对象。因此,要使用await关键字来执行awaitable(如,协程对象、Tasks、Futures等)

正确的协程函数定义

async def f(x):
    y = await z(x)  # OK - `await` and `return` allowed in coroutines
    return y

async def g(x):
    yield x  # OK - this is an async generator

async def m(x):
    yield from gen(x)  # No - SyntaxError,在async def 中,不能使用yield from

def m(x):
    y = await z(x)  # Still no - SyntaxError (no `async def` here)
    return y

2.2 awaitable(可等待对象)

能在 await表达式中使用的对象。可以是 coroutine 或是具有 __await__()方法的对象。
主要的awaitable对象:

  1. 协程对象
  2. Tasks
  3. Futures

2.3 Event loop(事件循环)

事件循环是每个 asyncio 应用的核心。 事件循环会运行异步任务和回调,执行网络 IO 操作,以及运行子进程。

开发者通常应当使用高层级的 asyncio 函数,例如asyncio.run(),最好不要引用loop`对象或调用其方法。

await 关键字的意思是,当遇到awaitawaitable对象,表明需要等待awaitable执行完成之后,才能继续执行此协程的剩余部分。遇到await 表示需要中断当前协程的执行(例如,遇到耗时的IO操作,不能白白浪费CPU啊),转而去执行其他协程了。

2.4 Tasks

用来并发地调度协程。 使用asyncio.create_task()可以将一个协程包装成一个Task,并自动将该协程的Task加入event loop中。

import asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():
    print(f"started at {time.strftime('%X')}")

    await say_after(1, 'hello')
    await say_after(2, 'world')

    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())

输出如下。用了3秒,并不是异步的。

started at 17:13:52
hello
world
finished at 17:13:55

现在我们把协程搞成Task。

async def main():
    task1 = asyncio.create_task(
        say_after(1, 'hello'))

    task2 = asyncio.create_task(
        say_after(2, 'world'))

    print(f"started at {time.strftime('%X')}")

    # Wait until both tasks are completed (should take
    # around 2 seconds.)
    await task1
    await task2

    print(f"finished at {time.strftime('%X')}")

输出如下:用时2s,表明是异步的了。

started at 17:14:32
hello
world
finished at 17:14:34

创建一个Task

 # 第1种方式 ,推荐使用
 asyncio.create_task()
 # 第二种方式
 loop = asyncio.get_event_loop()
 loop.create_task()
 # 第三种方式
 asyncio.ensure_future()

2.5 Futures

一个 Future 是一个低级的awaitable对象,用来代表一个异步运算的最终结果。线程不安全。

Future 是一个 awaitable对象。协程可以等待Future对象直到它们有结果或异常或被取消。

3. 简单使用

#!/usr/bin/env python3
# countasync.py

import asyncio

async def count():
    print("One")
    await asyncio.sleep(1)
    print("Two")

async def main():
    await asyncio.gather(count(), count(), count())

if __name__ == "__main__":
    import time
    s = time.perf_counter()
    asyncio.run(main())
    elapsed = time.perf_counter() - s
    print(f"{__file__} executed in {elapsed:0.2f} seconds.")

输出结果如下:

$ python3 countasync.py
One
One
One
Two
Two
Two
countasync.py executed in 1.01 seconds.

从上面的输出顺序,我们也能发现其实异步执行的了。上面的代码,将3个count()(其实是3个协程对象)加入了event loop当第一个协程遇到await asyncio.sleep(1)时,会将执行权交还给event loop,让event loop 安排一个协程获取一段时间的执行权。

如果是同步编程:

#!/usr/bin/env python3
# countsync.py

import time

def count():
    print("One")
    time.sleep(1)
    print("Two")

def main():
    for _ in range(3):
        count()

if __name__ == "__main__":
    s = time.perf_counter()
    main()
    elapsed = time.perf_counter() - s
    print(f"{__file__} executed in {elapsed:0.2f} seconds.")

输出如下:

$ python3 countsync.py
One
Two
One
Two
One
Two
countsync.py executed in 3.01 seconds.

async def定义了一个协程函数。
await关键词将执行权交还给event loop。如下代码所示,在g()中遇到 await f(),那么会将执行权交还给event loop,让其安排其他协程执行,并将g()挂起,直到得到了f()的返回值。

async def g():
    # Pause here and come back to g() when f() is ready
    r = await f()
    return r

Python 3.7以上的版本中,事件循环的整个管理由一个函数隐式处理(asyncio.run(main())

Python 3.7中引入的asyncio.run()负责获取事件循环,运行任务直到将其标记为完成,然后关闭事件循环。

使用get_event_loop(),可以更轻松地管理asyncio事件循环。 典型的模式如下所示:

loop = asyncio.get_event_loop()
try:
    loop.run_until_complete(main())
finally:
    loop.close()

在较早的示例中,您可能会看到loop.get_event_loop()随处可见,但是除非您特别需要微调对事件循环管理的控制,否则asyncio.run()对于大多数程序而言就足够了。

以下是事件循环的一些要点

  1. 在将协程安排进事件循环之前,协程并不会做太多事情
    通过main()这种调用方式,只会得到一个协程对象,并不会真正的执行。
>>> import asyncio

>>> async def main():
...     print("Hello ...")
...     await asyncio.sleep(1)
...     print("World!")

>>> routine = main()
>>> routine
<coroutine object main at 0x1027a6150>

通过asyncio.run()在事件循环中调度main协程的执行。

>>> asyncio.run(routine)
Hello ...
World!
  1. 默认情况下,异步IO事件循环在单个线程和单个CPU内核上运行。 通常,在一个CPU内核中运行一个单线程事件循环绰绰有余。 还可以跨多个内核运行事件循环。
  2. 事件循环是可插拔的。 也就是说,如果您确实需要,可以编写自己的事件循环实现,并使它运行相同的任务。 这在uvloop软件包中得到了很好的演示,该软件包是Cython中事件循环的实现。

协程与多线程/多进程相比有何优劣?

  1. 协程执行效率极高,没有线程切换,因此没有线程切换的开销。
  2. 通常,协程只在一个线程中的不同代码块进行切换,不需要多线程的锁机制,效率高。

由于协程只使用1个线程,那岂不是浪费了多核CPU?

可以使用多进程+协程。充分利用CPU资源。

4. 参考文献

[1] 这篇写的真的很不错 Python异步IO之协程(一):从yield from到async的使用
[2] Async IO in Python: A Complete Walkthrough
[3] 知乎 怎样理解阻塞非阻塞与同步异步的区别? YI LU的回答
[4] Python 官方文档 Asynchronous I/O
[5] 小破站 2小时学会python asyncio【花39大洋买的课程】
[6] 阮一峰的网络日志 Python 异步编程入门
[7] 廖雪峰的官方网站 asyncio

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值