0
点赞
收藏
分享

微信扫一扫

异步编程

近年来,异步编程取得了很大的发展。在 Python 3.5 中,它终于有了一些语法特性来巩

固异步执行的概念。但这并不意味着异步编程只能从 Python 3.5 开始。早期提供了很多库和

框架,其中大多数来源于旧版本的 Python 2。甚至还有一个称为 Stackless 的 Python 的整体替

代实现(参见第 1 章),它集中关于这种单一的编程方法。其中一些解决方案,如 Twisted、

Tornado 或 Eventlet,仍然有巨大的和活跃的社区,真的值得了解。无论如何,从 Python

3.5 开始,异步编程比以前更容易。因此,预计其内置的异步功能将取代大多数的旧工具,

或者外部项目将逐渐转变为基于 Python 内置的一种高级框架。

当试图解释什么是异步编程时,最简单的方法是将这种方法看作类似线程但不涉及系统调

度。这意味着异步程序可以并发地处理问题,但是其上下文在内部而不是由系统调度程序切换。

但是,当然,我们不使用线程来并发地处理异步程序中的工作。大多数解决方案使用不

同的概念,并且根据实现,它被命名不同。用于描述这种并发程序实体的一些示例名称如下。

• Green threads 或 greenlets(greenlet, gevent,或 eventlet 项目)。

• Coroutines(Python 3.5 原生异步编程)。

• Tasklets(Stackless Python)。

这些主要是相同的概念,但通常以一种不同的方式实现。由于显而易见的原因,在本

节中,我们将仅集中在 Python 原生支持的协程上,从版本 3.5 开始。

协同多任务与异步 I/O

协同多任务(cooperative multitasking)是异步编程的核心。在这种类型的计算机多任

务中,启动上下文切换(到另一个进程或线程)不是操作系统的责任,而是每个进程在空

闲时自动释放控制以允许同时执行多个程序。这就是为什么它被称为协同。所有进程都需

要协同才能顺利处理多任务。

这种多任务模型有时在操作系统中使用,但现在几乎没有作为系统级的解决方案。这

是因为一个设计不良的服务可能很容易地破坏整个系统的稳定性。由操作系统直接管理的

上下文切换的线程和进程调度,现在是系统级并发的主要方法。但是协同多任务在应用程序级别上仍然是一个极好的并发工具。

当谈到在应用程序级上的协同多任务时,我们不处理需要释放控制的线程或进程,因为所有

的执行都包含在单个进程和线程中。相反,我们有多个任务(coroutines、tasklets 以及

green threads)将控制释放到处理协同任务的单个函数。这个函数通常是某种事件循环。

为了避免以后的混乱(由于 Python 术语),从现在开始,我们将这样的并发任务称为协程

(coroutines)。协同多任务中最重要的问题是何时释放控制。在大多数异步应用程序中,控制在

I/O 操作时被调度程序或事件循环所释放。无论程序从文件系统读取数据还是通过套接字进行

通信,这种 I/O 操作总是与进程变为空闲时的某些等待时间有关。等待时间取决于外部资源,

所以它是释放控制的好机会,以便其他协程可以做它们的工作,直到它们也需要等待。

这使得这样的方法在行为上与在 Python 中实现多线程的方式有点类似。我们知道 GIL

串行化 Python 线程,但它也在每个 I/O 操作上释放。主要区别是 Python 中的线程被实现为

系统级线程,因此操作系统可以抢占当前运行的线程,并在任何时间点控制另一个线程。

在异步编程中,任务不会被主事件循环抢占。这就是为什么这种多任务的风格也被称为非

抢占式多任务(non-preemptive multitasking)。

当然,每个 Python 应用程序都运行在有其他进程竞争资源的操作系统上。这意味着操

作系统总是有权抢占整个进程并将控制权交给另一个进程。但是当我们的异步应用程序运

行回来时,它会从系统调度程序进入时暂停的相同位置继续运行。这就是为什么协同程序

仍被认为是非抢占式的。

Python 中的 async 和 await 关键字

async 和 await 关键字是 Python 异步编程的主要构建块。

在 def 语句之前使用的 async 关键字就定义了一个新的协程。协程函数的执行可以在严格

定义的情况下暂停和恢复。它的语法和行为与生成器非常相似(参见第 2 章)。事实上,生成器

需要在 Python 的旧版本中使用以实现协同程序。下面是使用 async 关键字的函数声明的示例:

async def async_hello():

print("hello, world!")

使用 async 关键字定义的函数是特殊的。当被调用时,它们不执行里面的代码,而是

返回一个协程对象如下所示:

>>> async def async_hello():

... print("hello, world!")

...

>>> async_hello()

<coroutine object async_hello at 0x1014129e8>

在事件循环中调度其执行之前,协程对象不执行任何操作。asyncio 模块可用于提供

基本的事件循环实现,以及许多其他异步实用程序,如下所示:

>>> import asyncio

>>> async def async_hello():

... print("hello, world!")

...

>>> loop = asyncio.get_event_loop()

>>> loop.run_until_complete(async_hello())

hello, world!

>>> loop.close()

显然,因为我们只创建了一个简单的协程,所以我们的程序中没有涉及并发。为了看

到真正的并发,我们需要创建更多的由事件循环执行的任务。

可以通过调用 loop.create_task()方法或者通过使用 asyncio.wait()函数提

供另一个对象来等待来将新任务添加到循环中。我们将使用后一种方法,并尝试异步打印

使用 range()函数生成的一系列数字如下:

import asyncio

async def print_number(number):

print(number)

if __name__ == "__main__":

loop = asyncio.get_event_loop()

loop.run_until_complete(

asyncio.wait([

print_number(number)

for number in range(10)

])

)

loop.close()

asyncio.wait()函数接受协程对象列表,并立即返回。结果是产生表示未来结果

(futures)的对象的生成器。顾名思义,它用于等待所有提供的协程完成。它返回一个

生成器而不是一个协程对象的原因是为了保持对以前 Python 版本的向后兼容性,这将在后

面解释。运行此脚本的结果可能如下:

$ python asyncprint.py

0

7

8

3

9

4

1

5

2

6

我们可以看到,数字不是按照我们创建协程的顺序打印的。但这正是我们想要实现的。

在 Python 3.5 中添加的第二个重要关键字是 await。它用于等待协程或未来(future)

的结果(稍后解释),并释放对事件循环的执行控制。为了更好地理解它是如何工作的,我

们需要回顾一个更复杂的代码示例。

假设我们想创建两个协程,它们将在循环中执行一些简单的任务。

• 随机等待几秒。

• 打印参数提供的一些文本以及睡眠时间。

让我们以一个简单的实现开始,该实现有一些并发问题,我们以后尝试通过额外的

await 使用进行改进,示例如下:

import time

import random

import asyncio

async def waiter(name):

for _ in range(4):

time_to_sleep = random.randint(1, 3) / 4

time.sleep(time_to_sleep)

print(

"{} waited {} seconds"

"".format(name, time_to_sleep)

)

async def main():

await asyncio.wait([waiter("foo"), waiter("bar")])

if __name__ == "__main__":

loop = asyncio.get_event_loop()

loop.run_until_complete(main())

loop.close()

当在终端中执行(用时间命令测量时间)时,它可能给出以下输出:

$ time python corowait.py

bar waited 0.25 seconds

bar waited 0.25 seconds

bar waited 0.5 seconds

bar waited 0.5 seconds

foo waited 0.75 seconds

foo waited 0.75 seconds

foo waited 0.25 seconds

foo waited 0.25 seconds

real 0m3.734s

user 0m0.153s

sys 0m0.028s

我们可以看到,两个协程都完成了它们的执行,但不是以异步方式。原因是它们都使

用阻塞但不将控件释放到事件循环的 time.sleep()函数。这将在多线程设置中更好地工

作,但现在我们不想使用线程。那么我们如何解决这个问题呢?

答案是使用 asyncio.sleep(),它是 time.sleep()的异步版本,并使用 await

关键字等待其结果。我们已经在 main()函数的第一个版本中使用了这个语句,但它只是

为了提高代码的清晰度。它显然没有改善我们的实现的并发性。让我们看看使用 await

asyncio.sleep()的 waiter 协程的改进版本如下:

async def waiter(name):

for _ in range(4):

time_to_sleep = random.randint(1, 3) / 4

await asyncio.sleep(time_to_sleep)

print(

"{} waited {} seconds"

"".format(name, time_to_sleep)

)

如果我们运行更新的脚本,我们可以看到两个函数的输出如何相互交互如下:

$ time python corowait_improved.py

bar waited 0.25 seconds

foo waited 0.25 seconds

bar waited 0.25 seconds

foo waited 0.5 seconds

foo waited 0.25 seconds

bar waited 0.75 seconds

foo waited 0.25 seconds

bar waited 0.5 seconds

real 0m1.953s

user 0m0.149s

sys 0m0.026s

这种简单改进的额外优点是代码运行地更快。总执行时间小于所有休眠时间的总和,

因为协程会协同地释放控制。

举报

相关推荐

0 条评论