asyncio 不完全指北(四)

书接上文。

同步原语

虽然使用 的程序通常都以单线程运行,但仍然可以作为并发程序。每个协程或任务可以根据来自 I / O 或其他外部事件的延迟和中断以不可预测的顺序执行。为了支持安全并发,和 和 模块一样, 包含了一些相同的低级原语的实现。

锁对共享资源的访问提供了保护。只有锁的持有者才能使用资源。第二次及以上获取锁的尝试将被阻止,因此每次只有一个持有者:

可以使用 持有一个锁,并用 释放,就像 的做法一样;同时也可以像 一样,用带有 的异步上下文处理器来持有并释放一个锁:

Event

与 类似,用于允许多个协程等待某个事件发生,而不需要监听一个特定值来实现类似通知的功能:

与锁一样, 和 都会等待 被设置。不同之处在于,它们可以在 状态发生变化时立即启动,并且它们不需要获取 对象的唯一使用权:

Condition

的作用类似于 ,不同之处在于, 不会唤醒所有等待中的协程,唤醒的数量由 的参数控制:

这个例子中启动了五个 的消费者。每个都使用 方法等待通知它们继续的消息。 通知一个消费者,然后通知两个消费者,最后通知所有剩余的消费者:

Queue

为协程提供了一个先进先出的数据结构,类似于与多线程中的 ,多进程中的 :

使用 添加项或使用 获取并删除项都是异步操作,因为队列大小可能是固定的(阻塞添加操作),或者队列可能是空的(阻塞获取项的操作):

用 Protocol 抽象类实现异步 I / O

到目前为止,这些示例都避免了将并发和 I / O 操作混合在一起,一次只关注一个概念。但是,在 I / O 阻塞时切换上下文是 的主要使用情形之一。在已经介绍的并发概念的基础上,本节将实现简单的 echo 服务器程序和客户端程序。客户端可以连接到服务器,发送一些数据,然后接收与响应相同的数据。每次启动 I / O 操作时,执行代码都会放弃对事件循环的控制,从而允许其他任务运行,直到 I / O 操作就绪。

Echo 服务器

服务器首先导入所需的 和 模块,然后创建事件循环对象:

然后定义了一个 的子类,用来处理与客户端的通信。 对象的方法是基于与服务器 socket 关联的事件调用的:

每个新的客户端连接都会触发对 的调用。 参数是 的实例,它提供了使用 socket 进行异步 I / O 的抽象。不同类型的通信提供不同的 实现,所有这些实现都具有相同的 API。例如,有单独的 类用于与 socket 通信、与子进程通过管道通信。传入客户端的地址可以通过 的 获取,这是一种特定于实现的方法:

建立连接后,当数据从客户端发送到服务器时,将调用协议的 方法将数据传入以进行处理。数据以字节串的形式传递,由应用程序以适当的方式对其进行解码。在这里记录结果,然后通过调用 立即将响应发送回客户端:

某些 支持特殊的文件结束标识符(EOF)。遇到 EOF 时,将调用 方法。在这个实现中,EOF 被发送回客户端来表示它已被接收。由于并非所有 都支持显式 EOF,因此 首先询问 发送 EOF 是否安全:

当连接关闭时,无论是正常关闭还是错误关闭,都会调用 的 方法。如果发生错误,参数会包含适当的异常对象,否则为 :

启动服务器有两个步骤。首先,应用程序告诉事件循环要使用的 类以及要侦听的主机名和 socket,用来创建新的服务器对象。 方法是协程,因此必须由事件循环处理结果,才能真正的启动服务器。然后,协程完成后产生了一个绑定到事件循环的 实例:

然后,需要运行事件循环以处理事件和客户端请求。对于长期运行的服务, 方法是最简单的方法。当事件循环停止时,无论是通过应用程序代码还是通过发信号通知进程,服务器都可以关闭,以便正确清理 socket,然后可以关闭事件循环,以便在程序退出之前完成对任何其他事务的处理:

Echo 客户端

使用 类构造客户端非常类似于构造服务器。首先导入所需的 和 模块,然后创建事件循环对象:

客户端 类定义了与服务器相同的方法,但实现方式不同。类构造函数接受两个参数,一个是要发送的消息列表,另一个是 的实例,用于通过接收来自服务器的响应来表明客户端已经完成了一个工作周期:

当客户端成功连接到服务器时,它将立即开始通信。消息序列一次发送一条,尽管底层网络代码可以将多个消息组合成一个传输。当所有消息都用尽时,将发送 EOF。

虽然看起来数据都是立即发送的,但实际上 对象缓冲传出的数据,并在当 socket 的缓冲区准备好接收数据时设置回调来进行实际的传输。所有这些都是透明处理的,因此可以编写应用程序代码,就好像 I / O 操作正在立即发生一样:

收到来自服务器的响应时,将记录该响应:

当从服务器端接收到 EOF 或者连接被关闭时,本地 对象被关闭,并通过设置结果将 对象标记为完成:

通常, 类被传递到事件循环以创建连接。在这种情况下,由于事件循环没有向 构造函数传递额外参数的工具,因此需要 来包装客户端类,并传递要发送的消息列表和 的实例。然后,在调用 建立客户端连接时,将使用该新的可调用对象代替 类:

为了触发客户端运行,事件循环将调用一次创建客户端的协程,然后再调用一次指定给客户端的 实例,以便在完成后进行通信。使用这样的两个调用避免了客户端程序中的无限循环,客户端程序可能希望在完成与服务器的通信后退出。如果仅使用第一个调用来等待协程创建客户端,那它可能无法处理所有响应数据并正确清理与服务器的连接:

输出

在一个窗口中运行服务器而在另一个窗口中运行客户端。

客户端将产生以下输出:

虽然客户端总是单独发送消息,但客户端第一次运行时,服务器会收到一条大消息,并将该消息返回给客户端。根据网络的繁忙程度以及是否在准备所有数据之前刷新网络缓冲区,这些结果在后续运行中会有所不同:

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180803G1A1YI00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券