• 文章目录

    • 高级异步编程
      • 1 使用asyncio的信号量和锁
          • 信号量 asyncio.Semaphore
          • 锁 asyncio.Lock()
      • 2 使用asyncio的队列和协程间通信
      • 3 使用asyncio的定时器和超时机制
        • asyncio.ensure_future 和 asyncio.creat_task 有什么区别


高级异步编程

  • 使用asyncio的信号量和锁
  • 使用asyncio的队列和协程间通信
  • 使用asyncio的定时器和超时机制

1 使用asyncio的信号量和锁

  • 在异步编程中,经常需要控制并发度,控制同时运行的数量,例如限制同时发送请求的数量或者限制同时读写文件的数量等。
  • 为了实现这样的功能,asyncio提供了信号量的概念。
信号量 asyncio.Semaphore
  • 信号量是一种计数器,用来控制同时访问共享资源的数量。
  • 当计数器大于0时,允许访问共享资源并将计数器减1;
  • 当计数器等于0时,不允许访问共享资源,需要等待其他任务释放资源并增加计数器。

下面是一个使用信号量的示例,假设我们需要并发地向一个API发送请求,但是我们想限制同时发送请求的数量为5:

import asyncio
import aiohttp  # 导入 aiohttp 库用于异步HTTP请求

# 1 使用async/await 定义协程
async def fetch(url, sem):
    async with sem:  # 获取信号量,如果达到限制就等待
        async with aiohttp.ClientSession() as session:  # 创建一个异步HTTP会话
            async with session.get(url) as response:  # 发送异步HTTP GET请求
                return await response.text()  # 返回响应内容

# 1 使用async/await 定义协程
async def main():
    sem = asyncio.Semaphore(5)  # 创建一个信号量,限制同时发送请求的数量为5
    urls = [...]  # 存储待请求的url列表
    tasks = []
    for url in urls:
        task = asyncio.ensure_future(fetch(url, sem))  # 创建一个异步任务,传入url和信号量
        tasks.append(task)  # 将任务添加到任务列表
    responses = await asyncio.gather(*tasks)  # 并发执行任务,返回所有任务的结果列表


if __name__ == '__main__':
    
    # 前面讲过,如果新版本 python 运行此处报错,则将这两行换成:asyncio.run(main())
    loop = asyncio.get_event_loop()  # 2 使用 asyncio.get_event_loop() 创建事件循环
    loop.run_until_complete(main())  # 3 在事件循环 loop.run_until_complete 中执行协程,直到完成或发生异常
  • 在上面的代码中,我们使用了asyncio.Semaphore来创建了一个限制同时发送请求的数量为5的信号量sem
  • fetch函数中,我们使用async with语句来获取信号量sem
  • 如果sem的计数器大于0,允许发送请求,并将计数器减1;
  • 如果sem的计数器等于0,不允许发送请求,需要等待其他任务释放资源并增加计数器。
  • 这样就实现了限制同时发送请求的数量为5的功能。

再来一个多人共用一个双人饭桌的例子:

import asyncio
import random


async def eat(name, semaphore):
    time_eat = random.randint(0, 6)
    async with semaphore:
        print(f"{name} 开始吃饭")
        await asyncio.sleep(time_eat)
        print(f"{name} 离开了,吃了{time_eat}分钟")


async def main():
    semaphore = asyncio.Semaphore(2)
    names = ["jack", "rose", "mike", "chris", "eaton", "tylor", "justin", "christipher"]
    tasks = []
    for man in names:
        task = asyncio.create_task(eat(man, semaphore))
        tasks.append(task)
    await asyncio.gather(*tasks)


if __name__ == '__main__':
    asyncio.run(main())
  • 根据运行结果可以很容易理解,Semaphore(2)代表同一时间只能最多两人使用饭桌,所以大家会轮流使用饭桌
jack 开始吃饭
rose 开始吃饭
jack 离开了,吃了3分钟
mike 开始吃饭
rose 离开了,吃了6分钟
mike 离开了,吃了3分钟
chris 开始吃饭
eaton 开始吃饭
eaton 离开了,吃了0分钟
tylor 开始吃饭
chris 离开了,吃了4分钟
justin 开始吃饭
tylor 离开了,吃了6分钟
christipher 开始吃饭
justin 离开了,吃了5分钟
christipher 离开了,吃了6分钟

进程已结束,退出代码0
锁 asyncio.Lock()
  • 锁是一种互斥锁,用来保证同一时间只有一个任务可以访问共享资源。
  • 当任务获取锁时,其他任务需要等待该任务释放锁才能访问共享资源。

下面是一个使用的示例,假设我们需要并发地向一个文件写入数据,但是我们想保证同一时间只有一个任务可以访问文件:

import asyncio

# 1 使用async/await 定义协程
async def write_file(i, text, lock):
    # async with语句自动获取锁,离开时自动释放锁
    async with lock:
        await asyncio.sleep(0.5)
        with open("people.txt", 'a', encoding='utf-8') as f:
            f.write(text + ",")
        print(f"第{i}个人:{text} 已写入文件")

# 1 使用async/await 定义协程
async def main():
    lock = asyncio.Lock()
    data = ["jack", "rose", "mike", "chris", "eaton", "tylor", "justin", "christipher"]
    tasks = []
    # 遍历数据,为每个数据创建一个任务;元素依次返回,同时为每个元素生成一个索引
    for i, d in enumerate(data):
        task = asyncio.ensure_future(write_file(i + 1, d, lock))
        tasks.append(task)
    await asyncio.gather(*tasks)


if __name__ == '__main__':
    asyncio.run(main())
  • 在上面的代码中,我们使用了asyncio.Lock来创建了一个保证同一时间只有一个任务可以访问文件的锁lock。
  • write_file函数中,我们使用async with语句来获取锁lock
  • 代码中定义了两个异步函数:write_filemain
  • 它使用一个循环遍历data列表中的数据,并使用asyncio.ensure_future创建一个任务,将任务添加到任务列表中。
  • 最后,它使用asyncio.gather来等待所有任务执行完毕。
  • for i, d in enumerate(data): 代码解读
  • 这行代码使用了for循环和enumerate函数来迭代一个名为data的可迭代对象(例如列表、元组、集合等)。
  • enumerate函数将data中的元素依次返回,同时为每个元素生成一个索引,索引从0开始,每个元素都对应一个唯一的索引。
  • 在循环的每一次迭代中,i将会是当前元素的索引,d将会是当前元素的值。
  • 因此,这行代码的作用是将data中的元素逐一遍历,同时得到当前元素的索引和值。
  • 可以在循环中使用id来进行相关操作。

使用asyncio的信号量和锁可以保证同一时间只有一个任务可以访问文件,从而避免了多个任务同时访问文件导致的数据混乱和错误。这是一个非常好的异步编程实践,也是Python异步编程的高级应用。

2 使用asyncio的队列和协程间通信

  • 异步编程中,协程间通信是非常重要的一环。
  • asyncio提供了内置的队列来实现协程间的消息传递。

代码说明:

  • asyncio.Queue():创建一个asyncio队列对象。
  • queue.put(item):将item放入队列中。
  • queue.get():从队列中获取一个元素,如果队列为空,该操作会阻塞。
  • asyncio.create_task():创建一个协程任务。
  • asyncio.gather():等待多个协程任务完成。
  • queue.task_done():在处理完一个元素后,通知队列元素已经处理完成。
import asyncio

# 1 使用async/await 定义协程
async def producer(queue, max_items):  # `queue` 是队列对象,`max_items` 是最大生产数据量
    for i in range(max_items):
        # 生产者产生数据
        print(f'Producing item {i}')
        await queue.put(i)
        await asyncio.sleep(1)

    # 发送结束信号
    await queue.put(None)

    
# 1 使用async/await 定义协程
async def consumer(queue):
    while True:
        # 从队列中获取数据
        item = await queue.get()
        if item is None:
            # 如果是结束信号,就退出循环
            break

        # 消费者处理数据
        print(f'Consuming item {item}')
        await asyncio.sleep(0.5)

        
# 1 使用async/await 定义协程
async def main():
    # 创建一个asyncio队列对象。
    queue = asyncio.Queue()

    # 生产者和消费者协同工作
    producer_task = asyncio.create_task(producer(queue, 10))
    consumer_task = asyncio.create_task(consumer(queue))

    # 等待生产者完成
    await producer_task

    # 等待消费者完成
    await queue.join()
    await consumer_task

    
if __name__ == '__main__':
    # 2 使用 asyncio.run 创建事件循环
    # 3 在事件循环 asyncio.run 中执行协程
    asyncio.run(main())
  • 生产者函数中,使用 for 循环生成数据,并使用 await queue.put(i) 将数据放到队列中,生产完成后再发送一个结束信号 None
  • 消费者函数中,处理获取到的数据,然后调用 await asyncio.sleep(0.5) 让程序休眠 0.5 秒,以便观察程序运行效果。
  • async def main() 函数中,创建队列 queue = asyncio.Queue(),创建生产者任务和消费者任务,并通过 await 让生产者任务先完成。
  • 最后,等待队列中所有任务完成,然后让消费者任务完成。
  • 在示例中,由于producer的元素数量固定,所以程序可以正常退出。如果在producer的循环中使用无限循环,则需要在程序退出前手动取消任务,以避免无限等待。

在协程间通信中,还可以使用asyncio提供的EventCondition对象来进行通信和同步。但在大多数情况下,使用asyncio队列就足以满足需要了。

3 使用asyncio的定时器和超时机制

以下是使用asyncio的定时器超时机制的示例代码,注释已添加在代码中:

import asyncio

# 1 使用async/await 定义协程
import random


async def task():
    # 延时随机秒后打印消息
    time_task = random.randint(0, 5)
    await asyncio.sleep(time_task)
    print('Task completed')


# 1 使用async/await 定义协程
async def main():
    # 创建一个任务,把这个任务封装成一个 `Future` 对象
    t = asyncio.ensure_future(task())

    try:
        # 等待2秒,如果任务在2秒内没有完成,则超时
        await asyncio.wait_for(t, timeout=2)
    except asyncio.TimeoutError:
        # 如果超时,则取消任务
        t.cancel()
        print('Task timed out')


if __name__ == '__main__':
    asyncio.run(main())
  • 这个示例中,我们创建了一个协程任务 task,这个任务在3秒后会打印消息。
  • 然后我们使用 asyncio.ensure_future 把这个任务封装成一个 Future 对象,最后把这个 Future 对象传给 asyncio.wait_for 函数,设置超时时间为5秒。
  • 如果任务在5秒内完成,wait_for 函数会返回任务的结果;
  • 否则,wait_for 函数会抛出 asyncio.TimeoutError 异常。
  • main 函数中,我们使用 try...except 结构来捕获超时异常,并在超时的情况下取消任务。

这个示例展示了如何使用 asyncio.wait_for 函数实现协程任务的超时控制。在实际开发中,超时控制通常用于限制网络请求的响应时间,避免程序卡死或等待过久。

asyncio.ensure_future 和 asyncio.creat_task 有什么区别

  • asyncio.ensure_future()asyncio.create_task()都可以用来将协程对象封装成一个Future对象并且提交给事件循环,使其异步执行

共同点

  • 都是将协程任务封装成Future对象并加入事件循环中异步执行
  • 都是返回一个Task对象,可以用于取消任务、获取任务状态等操作
  • 都可以传入已经封装的Future对象,比如使用asyncio.Future()手动创建的对象

不同点

  • asyncio.ensure_future()可以接受协程对象或者Future对象作为参数,而asyncio.create_task()只能接受协程对象作为参数。
  • asyncio.create_task()从Python3.7开始引入,而asyncio.ensure_future()从Python3.4开始就已经存在。

特点

  • asyncio.ensure_future()会自动选择Task类或者Future类来封装传入的协程对象或者Future对象,具体取决于传入对象的类型。
  • 如果传入的是协程对象,那么它会自动选择Task类来封装
  • 如果传入的是Future对象,那么它会自动选择Future类来封装
  • asyncio.create_task()始终选择Task类来封装协程对象,不会自动选择Future

在实际使用中,建议使用asyncio.create_task()来创建Task对象,因为它可以确保封装的一定是Task对象,而且语义更加明确。在Python3.7之前,可以使用asyncio.ensure_future()来创建Task对象,但是需要注意传入的参数类型,以免出现意外的情况。