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())

对比与优点

  1. 状态隔离:

    • 传统编程(多线程 + TLS):每个线程有自己的独立状态,通过线程局部存储(threading.local)来实现。但这在异步编程中无效,因为协程在同一线程中执行。
    • ContextVar 编程:每个协程有自己的独立状态,通过上下文变量(ContextVar)来实现。即使在同一线程中并发执行,状态也能正确隔离。
  2. 并发编程的简化:

    • 传统编程:在多线程环境中,管理共享状态复杂且容易出错,需要小心处理锁和竞态条件。
    • ContextVar 编程:上下文变量自动管理状态隔离,不需要显式加锁,简化了代码编写和维护。
  3. 适用性:

    • 传统编程:适用于同步多线程编程,不适用于异步编程(如使用 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)执行的特殊方法。

让我们逐步了解它的概念、作用、通俗解释和使用举例。

  1. 概念和作用:

send方法是Python生成器对象的一个内置方法,它允许我们在生成器的执行过程中向其发送数据。

当生成器遇到yield表达式时,它会暂停执行并将值返回给调用方。通过调用生成器对象的send方法,我们可以将数据发送回生成器,从而继续执行生成器函数直到下一个yield表达式。

  1. 通俗解释:

生成器就像一台可暂停的机器,当它遇到yield时,它会暂停并将当前值传出来。

send方法就像是向这台机器喂入原料一样,允许我们在机器暂停时向其提供新的数据,从而使机器继续运行并产生下一个结果。

  1. 使用举例:

在给定的代码中,我们定义了一个run_generator_step函数,它接受一个生成器对象作为参数。

在函数内部,我们使用try/except块来捕获StopIteration异常,因为当生成器执行完毕时,调用send方法会引发该异常。

while True循环中,我们调用run_generator_step函数,将one_to_fivefive_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语句都暂停了函数的执行,等待接收下一种原料。

  1. 我们首先创建了一个生成器对象sandwich_maker,并使用next(sandwich_maker)启动它。这会执行生成器函数直到第一个yield语句。
  2. 然后,我们使用sandwich_maker.send("全麦面包")向生成器发送面包。生成器函数会从上次暂停的位置继续执行,直到遇到下一个yield语句。在这个过程中,它会打印出"有了 全麦面包 面包"。
  3. 接下来,我们继续使用send方法向生成器发送其他原料,如肉类、奶酪和蔬菜。每次调用send方法,生成器函数都会从上次暂停的位置继续执行,直到遇到下一个yield语句或函数终止。
  4. 我们使用 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 对象的状态。

  1. print('Checking future...') 打印 "Checking future..."。
  2. gen = future.__await__() 获取 future 对象的awaitable对象。
  3. gen.send(None) 向awaitable对象发送 None。如果 future 还没有设置结果,这个操作会暂停awaitable对象的执行,并打印 "Future is not done..."。
  4. if i == 1 检查计数器 i 是否等于 1。
  5. 如果 i == 1,打印 "Setting future value..."并调用 future.set_result('Finished!') 设置 future 对象的结果为 "Finished!"。
  6. i = i + 1 将计数器 i 加 1。
  7. 如果 future 对象已经设置了结果,gen.send(None) 会引发 StopIteration 异常,并将结果值作为异常值返回。
  8. except StopIteration as si 捕获 StopIteration 异常。
  9. print(f'Value is: {si.value}') 打印 future 对象的结果值。
  10. break 跳出无限循环。

所以输出结果如下:

Checking future...
Future is not done...
Checking future...
Future is not done...
Setting future value...
Checking future...
Value is: Finished!

解释:

  1. 第一次迭代,打印 "Checking future..."和 "Future is not done..."。
  2. 第二次迭代,同样打印 "Checking future..."和 "Future is not done..."。此时 i 等于 1,因此打印 "Setting future value..."并设置 future 对象的结果为 "Finished!"。
  3. 第三次迭代,打印 "Checking future..."。由于 future 对象已经设置了结果, 因此 gen.send(None) 会引发 StopIteration 异常。捕获异常后,打印 future 对象的结果值 "Value is: Finished!"。
  4. 由于 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
  • BaseSelectorselectors 模块中的选择器基类,用于管理 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 多路复用。

选择器的步骤

  1. 注册事件:通过 sel.register 将 I/O 对象(如 socket)注册到选择器,并指定要监视的事件类型(如 EVENT_READ
  2. 等待事件:通过 selector.select() 方法等待任何一个注册的事件发生
  3. 处理事件:当事件发生时,选择器返回事件列表。应用程序处理这些事件(如调用回调函数)。

总结

这段代码展示了如何使用选择器和自定义 Future 来实现一个异步服务器。选择器用于高效地监视多个 I/O 事件,Future 用于管理异步操作的结果。

通过这种方式,服务器可以在单线程中处理多个并发连接,且不需要阻塞等待。

 

posted @ 2024-06-05 22:55  AlphaGeek  阅读(31)  评论(0)    收藏  举报