Chapter14_高级asyncio
1、python的from contextvars import ContextVar中,ContextVar的概念的理解
ContextVar 是 Python 3.7 引入的 contextvars 模块中的一个类,它用于创建和管理上下文变量。
上下文变量是一种特殊的变量,它与执行的上下文(即运行时环境)相关联,而不是与线程或进程相关联。这使得它们非常适合用于异步编程和协程,其中传统的线程局部数据可能不够用。
概念:
ContextVar 允许你定义一个变量,这个变量的值在不同的执行上下文中可以不同。
每个 ContextVar 实例都有一个相关联的默认值,但是如果在当前上下文中没有显式设置值,它将返回默认值。
作用:
ContextVar 的主要作用是提供一种机制,使得你可以在不同的执行上下文之间传递数据,
而不需要通过复杂的参数传递或者全局状态管理。这对于编写可维护和可扩展的异步代码特别有用。
通俗解释:
想象一下你在编写一个多任务的程序,每个任务都有自己的一些局部变量。
在传统的编程模型中,这些变量可能需要通过参数传递给需要它们的方法,或者存储在全局状态中。
但是,使用 ContextVar,你可以定义一个变量,这个变量在每个任务中可以有自己的值,而且这些值是自动和任务关联的。
当你切换到另一个任务时,相关的变量值也会随之改变。
使用举例说明:
下面是一个简单的示例,展示如何使用 ContextVar 在不同的协程中管理独立的上下文状态:
from contextvars import ContextVar import asyncio # 定义一个上下文变量 request_id = ContextVar('request_id') async def handle_request(name, id): # 为每个请求设置不同的上下文变量值 request_id.set(id) await asyncio.sleep(1) # 模拟处理请求的时间 print(f"Request {name} has request ID: {request_id.get()}") async def main(): # 启动多个协程处理不同的请求 await asyncio.gather( handle_request('A', 101), handle_request('B', 202), handle_request('C', 303), ) asyncio.run(main())
在这个示例中,request_id 是一个 ContextVar,它在每个请求处理函数 handle_request 中被设置为不同的值。
每个请求在其执行过程中,都可以访问到自己的 request_id,即使它们是同时运行的协程。
运行结果会是:
Request A has request ID: 101 Request B has request ID: 202 Request C has request ID: 303
每个请求都保留了自己独立的上下文状态,没有发生混淆。
2、对比下说明传统的编程 和 使用了ContextVar后的编程带来的方便,这样更容易加深理解。
传统编程
在传统的多线程编程中,我们通常会使用线程局部存储(Thread Local Storage, TLS)来管理每个线程的独立状态。例如:
import threading # 使用 thread-local 数据 thread_local = threading.local() def handle_request(name, id): # 为每个请求设置不同的线程局部变量值 thread_local.request_id = id print(f"Request {name} has request ID: {thread_local.request_id}") threads = [] for name, id in [('A', 101), ('B', 202), ('C', 303)]: t = threading.Thread(target=handle_request, args=(name, id)) threads.append(t) t.start() for t in threads: t.join()
在这个例子中,每个线程都有自己的 request_id,确保各线程的状态互不干扰。这种方法在同步的多线程编程中很有效,但在异步编程中却存在局限性。
使用 ContextVar 的编程
在异步编程中,多个协程可以在同一个线程中并发执行,因此线程局部存储不再适用。使用 ContextVar 可以方便地解决这个问题:
from contextvars import ContextVar import asyncio # 定义一个上下文变量 request_id = ContextVar('request_id') async def handle_request(name, id): # 为每个请求设置不同的上下文变量值 request_id.set(id) await asyncio.sleep(1) # 模拟处理请求的时间 print(f"Request {name} has request ID: {request_id.get()}") async def main(): # 启动多个协程处理不同的请求 await asyncio.gather( handle_request('A', 101), handle_request('B', 202), handle_request('C', 303), ) asyncio.run(main())
对比与优点
-
状态隔离:
- 传统编程(多线程 + TLS):每个线程有自己的独立状态,通过线程局部存储(
threading.local)来实现。但这在异步编程中无效,因为协程在同一线程中执行。 ContextVar编程:每个协程有自己的独立状态,通过上下文变量(ContextVar)来实现。即使在同一线程中并发执行,状态也能正确隔离。
- 传统编程(多线程 + TLS):每个线程有自己的独立状态,通过线程局部存储(
-
并发编程的简化:
- 传统编程:在多线程环境中,管理共享状态复杂且容易出错,需要小心处理锁和竞态条件。
ContextVar编程:上下文变量自动管理状态隔离,不需要显式加锁,简化了代码编写和维护。
-
适用性:
- 传统编程:适用于同步多线程编程,不适用于异步编程(如使用
asyncio)。 ContextVar编程:适用于异步编程,特别是在处理高并发 I/O 密集型任务时表现出色。
- 传统编程:适用于同步多线程编程,不适用于异步编程(如使用
小结
使用 ContextVar 后,异步编程中的上下文管理变得更加简单和直观。
每个协程可以独立维护自己的状态,避免了复杂的锁管理和竞态条件,显著提升了代码的可读性和维护性。
传统编程方式在异步环境下已经不再适用,而 ContextVar 恰好填补了这一空白。
3、这里的send方法的作用的理解
from typing import Generator def generator(start: int, end: int): for i in range(start, end): yield i
one_to_five = generator(1, 5) five_to_ten = generator(5, 10)
def run_generator_step(gen: Generator[int, None, None]): #A try: return gen.send(None) except StopIteration as si: return si.value
while True: #B one_to_five_result = run_generator_step(one_to_five) five_to_ten_result = run_generator_step(five_to_ten) print(one_to_five_result) print(five_to_ten_result)
if one_to_five_result is None and five_to_ten_result is None: break
在这段代码中,send方法是一个用于控制生成器(generator)执行的特殊方法。
让我们逐步了解它的概念、作用、通俗解释和使用举例。
- 概念和作用:
send方法是Python生成器对象的一个内置方法,它允许我们在生成器的执行过程中向其发送数据。
当生成器遇到yield表达式时,它会暂停执行并将值返回给调用方。通过调用生成器对象的send方法,我们可以将数据发送回生成器,从而继续执行生成器函数直到下一个yield表达式。
- 通俗解释:
生成器就像一台可暂停的机器,当它遇到yield时,它会暂停并将当前值传出来。
send方法就像是向这台机器喂入原料一样,允许我们在机器暂停时向其提供新的数据,从而使机器继续运行并产生下一个结果。
- 使用举例:
在给定的代码中,我们定义了一个run_generator_step函数,它接受一个生成器对象作为参数。
在函数内部,我们使用try/except块来捕获StopIteration异常,因为当生成器执行完毕时,调用send方法会引发该异常。
在while True循环中,我们调用run_generator_step函数,将one_to_five和five_to_ten生成器对象作为参数传递。
在第一次调用send(None)时,这会启动生成器并执行到第一个yield语句。后续的send调用会将None发送回生成器,从而继续执行生成器函数直到下一个yield语句或生成器终止。
我们打印出每个生成器对象返回的值,并检查它们是否为None。如果两个生成器都返回None,说明它们已经执行完毕,我们就可以退出循环。
这个示例展示了如何使用send方法控制生成器的执行,并在生成器暂停时向其发送数据。
虽然在这个特定示例中,我们只是发送None来继续执行生成器,但是在实际应用中,我们可以向生成器发送任何所需的数据,从而实现更复杂的控制流程。
总的来说,send方法为我们提供了一种与生成器交互的方式,使得生成器不仅可以生成值,还可以接收值,从而增强了生成器的灵活性和功能。
4、追问:send具体用法举例说明,要求通俗易懂
通过一个通俗易懂的例子来说明send方法的具体用法。
想象一下,你正在制作一份三明治。制作过程可以分为多个步骤,每个步骤都需要一些原料。
我们可以将整个制作过程看作一个生成器函数,每个步骤对应生成器函数中的一个yield语句。
def make_sandwich(): bread = yield # 等待提供面包 print(f"有了 {bread} 面包") meat = yield # 等待提供肉类 print(f"在面包里加入了 {meat}") cheese = yield # 等待提供奶酪 print(f"再加入了 {cheese} 奶酪") veggie = yield # 等待提供蔬菜 print(f"最后放入了 {veggie} 蔬菜") print("三明治做好了,开吃!") # 创建生成器对象 sandwich_maker = make_sandwich() # 启动生成器,获取第一个 yield next(sandwich_maker) # 输出: 无 # 向生成器发送面包 sandwich_maker.send("全麦面包") # 输出: 有了 全麦面包 面包 # 向生成器发送肉类 sandwich_maker.send("火鸡肉") # 输出: 在面包里加入了 火鸡肉 # 向生成器发送奶酪 sandwich_maker.send("切达奶酪") # 输出: 再加入了 切达奶酪 奶酪 try: # 向生成器发送蔬菜 sandwich_maker.send("生菜") # 输出: 最后放入了 生菜 蔬菜 # 三明治做好了,开吃! except StopIteration: print("生成器执行完毕")
在这个例子中,make_sandwich函数是一个生成器函数,它模拟了制作三明治的步骤。每个yield语句都暂停了函数的执行,等待接收下一种原料。
- 我们首先创建了一个生成器对象
sandwich_maker,并使用next(sandwich_maker)启动它。这会执行生成器函数直到第一个yield语句。 - 然后,我们使用
sandwich_maker.send("全麦面包")向生成器发送面包。生成器函数会从上次暂停的位置继续执行,直到遇到下一个yield语句。在这个过程中,它会打印出"有了 全麦面包 面包"。 - 接下来,我们继续使用
send方法向生成器发送其他原料,如肉类、奶酪和蔬菜。每次调用send方法,生成器函数都会从上次暂停的位置继续执行,直到遇到下一个yield语句或函数终止。 -
我们使用
try/except块来捕获StopIteration异常。当生成器函数执行完毕后,最后一次调用sandwich_maker.send("生菜")会引发StopIteration异常,我们在except块中打印出 "生成器执行完毕" 的消息。这样一来,代码就可以正常运行,并且不会引发任何异常。输出结果如下:
有了 全麦面包 面包
在面包里加入了 火鸡肉
再加入了 切达奶酪 奶酪
最后放入了 生菜 蔬菜
三明治做好了,开吃!
生成器执行完毕
总之,在使用 send 方法时,我们需要谨记一旦生成器函数执行完毕, 再次调用 send 方法会引发 StopIteration 异常。
因此,我们必须使用 try/except 块来正确捕获和处理这种情况
6、逐行解释下如下代码的输出结果,尤其是最后几行。 【from example14_9.py】
from example14_8 import CustomFuture future = CustomFuture() i = 0 while True: try: print('Checking future...') gen = future.__await__() gen.send(None) print('Future is not done...') if i == 1: print('Setting future value...') future.set_result('Finished!') i = i + 1 except StopIteration as si: print(f'Value is: {si.value}') break
输出结果:
Checking future... Future is not done... Checking future... Future is not done... Setting future value... Checking future... Value is: Finished!
下面将逐行解释这段代码的输出结果,尤其是最后几行。
future = CustomFuture()
这一行创建了一个 CustomFuture 对象,用于模拟异步操作的结果。
i = 0
初始化一个计数器变量 i,用于控制循环次数。
while True: try: print('Checking future...') gen = future.__await__() gen.send(None) print('Future is not done...') if i == 1: print('Setting future value...') future.set_result('Finished!') i = i + 1 except StopIteration as si: print(f'Value is: {si.value}') break
这是一个无限循环,每次迭代都会检查 future 对象的状态。
print('Checking future...')打印 "Checking future..."。gen = future.__await__()获取future对象的awaitable对象。gen.send(None)向awaitable对象发送None。如果future还没有设置结果,这个操作会暂停awaitable对象的执行,并打印 "Future is not done..."。if i == 1检查计数器i是否等于 1。- 如果
i == 1,打印 "Setting future value..."并调用future.set_result('Finished!')设置future对象的结果为 "Finished!"。 i = i + 1将计数器i加 1。- 如果
future对象已经设置了结果,gen.send(None)会引发StopIteration异常,并将结果值作为异常值返回。 except StopIteration as si捕获StopIteration异常。print(f'Value is: {si.value}')打印future对象的结果值。break跳出无限循环。
所以输出结果如下:
Checking future... Future is not done... Checking future... Future is not done... Setting future value... Checking future... Value is: Finished!
解释:
- 第一次迭代,打印 "Checking future..."和 "Future is not done..."。
- 第二次迭代,同样打印 "Checking future..."和 "Future is not done..."。此时
i等于 1,因此打印 "Setting future value..."并设置future对象的结果为 "Finished!"。 - 第三次迭代,打印 "Checking future..."。由于
future对象已经设置了结果, 因此gen.send(None)会引发StopIteration异常。捕获异常后,打印future对象的结果值 "Value is: Finished!"。 - 由于
break语句,循环终止。
总的来说,这段代码模拟了异步操作的等待过程。在第一次和第二次迭代中,异步操作还没有完成,因此打印 "Future is not done..."。
在第三次迭代中,异步操作完成,结果被设置为 "Finished!"。最后,程序打印出异步操作的结果值并退出循环。
7、如下代码如何理解?尤其是关于选择器selector的概念。 [example14_10.py]
import functools import selectors import socket from example14_8 import CustomFuture from selectors import BaseSelector def accept_connection(future: CustomFuture, connection: socket): # A print(f'We got a connection from {connection}!') future.set_result(connection) async def sock_accept(sel: BaseSelector, sock) -> socket: # B print('Registering socket to listen for connections') future = CustomFuture() sel.register(sock, selectors.EVENT_READ, functools.partial(accept_connection, future)) print('Pausing to listen for connections...') connection: socket = await future return connection async def main(sel: BaseSelector): sock = socket.socket() sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('127.0.0.1', 8000)) sock.listen() sock.setblocking(False) print('Waiting for socket connection!') connection = await sock_accept(sel, sock) # C print(f'Got a connection {connection}!') selector = selectors.DefaultSelector() coro = main(selector) while True: # D try: state = coro.send(None) events = selector.select() for key, mask in events: print('Processing selector events...') callback = key.data callback(key.fileobj) except StopIteration as si: print('Application finished!') break
这段代码实现了一个基于 selectors 模块和自定义 Future 的异步服务器,能够在异步处理连接请求时使用选择器来监听和处理 I/O 事件。
下面我将详细解释每个部分及其作用,尤其是复杂的部分,如选择器的工作原理。
代码详解
导入模块
import socket from listing_14_8 import CustomFuture from selectors import BaseSelector
socket:用于创建和管理网络连接。CustomFuture:自定义的 Future 对象,类似于asyncio.Future。BaseSelector:selectors模块中的选择器基类,用于管理 I/O 事件。
接受连接的回调函数
def accept_connection(future: CustomFuture, connection: socket): print(f'We got a connection from {connection}!') future.set_result(connection)
accept_connection:当有新的连接到来时,被选择器调用。它接受一个future和一个connection,并将连接设置为 Future 的结果。
异步接受连接函数
async def sock_accept(sel: BaseSelector, sock) -> socket: print('Registering socket to listen for connections') future = CustomFuture() sel.register(sock, selectors.EVENT_READ, functools.partial(accept_connection, future)) print('Pausing to listen for connections...') connection: socket = await future return connection
sock_accept:注册 socket 以便选择器监听连接事件,暂停等待连接,直到 Future 完成。- 创建一个
CustomFuture对象。 - 使用选择器注册 socket,以监听
EVENT_READ事件,并将accept_connection作为回调函数。 await这个 Future,直到它被回调函数设置结果。
- 创建一个
主函数
async def main(sel: BaseSelector): sock = socket.socket() sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('127.0.0.1', 8000)) sock.listen() sock.setblocking(False) print('Waiting for socket connection!') connection = await sock_accept(sel, sock) print(f'Got a connection {connection}!')
main:设置并启动服务器,等待连接。- 创建并配置 socket。
- 绑定到指定地址并开始监听连接。
- 设置 socket 为非阻塞模式。
- 调用
sock_accept等待并处理连接。
选择器的使用
selector = selectors.DefaultSelector()
coro = main(selector)
selectors.DefaultSelector:创建一个默认的选择器实例,它会根据操作系统选择最合适的选择器实现。main函数被转换为一个协程对象coro。
事件循环
while True: try: state = coro.send(None) events = selector.select() for key, mask in events: print('Processing selector events...') callback = key.data callback(key.fileobj) except StopIteration as si: print('Application finished!') break
coro.send(None):推进协程执行到下一个await,并返回当前状态。selector.select():阻塞等待注册的 I/O 事件。for key, mask in events:处理每个事件。key.data:存储回调函数,这里是accept_connection。callback(key.fileobj):调用回调函数处理事件。
选择器的作用
选择器是用于高效地监视多个 I/O 事件(如读写事件、连接事件等)的机制。选择器能够在一个线程中管理多个 I/O 对象(如 sockets),
当任何一个对象准备好进行 I/O 操作时,选择器会通知应用程序,从而实现高效的 I/O 多路复用。
选择器的步骤
- 注册事件:通过
sel.register将 I/O 对象(如 socket)注册到选择器,并指定要监视的事件类型(如EVENT_READ)。 - 等待事件:通过
selector.select()方法等待任何一个注册的事件发生。 - 处理事件:当事件发生时,选择器返回事件列表。应用程序处理这些事件(如调用回调函数)。
总结
这段代码展示了如何使用选择器和自定义 Future 来实现一个异步服务器。选择器用于高效地监视多个 I/O 事件,Future 用于管理异步操作的结果。
通过这种方式,服务器可以在单线程中处理多个并发连接,且不需要阻塞等待。

浙公网安备 33010602011771号