Python面试官:你来解释一下协程的实现原理

本篇文章主要内容:

  • 协程的基本概念和Python协程语法
  • Python 协程常用的 API
  • 协程适用场景

1 基本概念

异步编程允许程序在等待 I/O 操作完成时继续执行其它任务,而不是被阻塞。异步编程是Python处理并发操作的强大方式,特别适合I/O密集型任务。Python的异步编程主要基于以下概念:

  • 协程(Coroutine):可以暂停执行并稍后恢复的函数
  • 事件循环(Event Loop): 管理和执行所有异步任务的核心调度器
  • 任务(Task): 协程的包装器,使协程可以被调度执行
  • Awaitable对象 : 可以在await表达式中使用的对象
  • Future对象: 表示尚未完成的计算结果

1.1 异步函数定义

使用async def关键字定义一个异步函数:

python 复制代码
async def my_async_function():
    print("Start of the function")
    await some_async_operation()
    print("End of the function")

1.2 await表达式

在异步函数中,使用await关键字等待另一个异步操作完成:

python 复制代码
async def fetch_data():
    print("Fetching data...")
    await asyncio.sleep(1)  # 模拟一个耗时操作
    print("Data fetched!")

1.3 运行协程

要运行一个协程,需要使用asyncio.run()函数:

python 复制代码
import asyncio
async def main():
    await fetch_data()
asyncio.run(main())

1.4 并发执行多个协程

可以使用asyncio.gather()来并发执行多个协程:

python 复制代码
async def task1():
    print("Task 1 started")
    await asyncio.sleep(2)
    print("Task 1 completed")
async def task2():
    print("Task 2 started")
    await asyncio.sleep(1)
    print("Task 2 completed")
async def main():
    await asyncio.gather(task1(), task2())
asyncio.run(main())

生成器协程与原生协程

在Python 3.7及以上版本中,推荐使用async def定义的原生协程。旧版本中也可以使用生成器协程,但需要使用asyncio.coroutine装饰器:

python 复制代码
import asyncio
@asyncio.coroutine
def old_style_coroutine():
    print("Old style coroutine started")
    yield from asyncio.sleep(1)
    print("Old style coroutine completed")
async def main():
    await old_style_coroutine()
asyncio.run(main())

2. Python 协程常用的 API

2.1 asyncio.run()

简化了运行协程的方式,自动管理事件循环。
示例:

python 复制代码
import asyncio
async def main():
    await asyncio.sleep(1)
    print("Hello, asyncio!")
asyncio.run(main())

2.2 asyncio.sleep()

用于暂停协程执行,模拟I/O操作。
示例:

python 复制代码
async def delay_print(message):
    await asyncio.sleep(1)
    print(message)
async def main():
    await asyncio.gather(
        delay_print("Hello"),
        delay_print("World")
    )
asyncio.run(main())

2.3 asyncio.gather()

将多个协程组合为一个,同时运行。
示例:

python 复制代码
async def func1():
    print("Function 1 started")
    await asyncio.sleep(2)
    print("Function 1 done")
async def func2():
    print("Function 2 started")
    await asyncio.sleep(1)
    print("Function 2 done")
async def main():
    await asyncio.gather(func1(), func2())
asyncio.run(main())

2.4 asyncio.EventLoop

管理协程的执行循环。常用方法包括:

  • loop.run_forever(): 开始事件循环,直到loop.stop()被调用。
  • loop.run_until_complete(): 执行直到协程完成。
    示例:
python 复制代码
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

事件循环通常用于网络编程。

2.5 asyncio.Task

用于管理协程任务。
示例:

python 复制代码
async def task_func():
    print("Task is running")
async def main():
    task = asyncio.create_task(task_func())
    await task
asyncio.run(main())

2.6 asyncio.Queue

在协程间安全地传递数据。
示例:

python 复制代码
async def producer(queue):
    await queue.put("Hello")
    await queue.put("World")
    await queue.put(None)
async def consumer(queue):
    while True:
        item = await queue.get()
        if item is None:
            break
        print(item)
async def main():
    queue = asyncio.Queue()
    await asyncio.gather(
        producer(queue),
        consumer(queue)
    )
asyncio.run(main())

2.7 asyncio.Event

用于协程间的同步。
示例:

python 复制代码
import asyncio
import time

async def waiter(event, name):
    print(f"等待者 {name} 开始等待事件")
    start_time = time.time()
    await event.wait()  # 等待事件被设置
    end_time = time.time()
    print(f"等待者 {name} 检测到事件发生,等待时间: {end_time - start_time:.2f}秒")

async def event_setter(event, delay):
    print(f"事件设置者开始工作,将在 {delay} 秒后设置事件")
    await asyncio.sleep(delay)  # 模拟一些工作
    print("事件设置者正在设置事件...")
    event.set()  # 设置事件,唤醒所有等待者
    print("事件已设置")

async def main():
    # 创建一个事件对象
    event = asyncio.Event()
    
    # 创建多个等待者任务
    waiters = [
        waiter(event, "A"),
        waiter(event, "B"),
        waiter(event, "C")
    ]
    
    # 创建事件设置者任务
    setter = event_setter(event, 3)
    
    # 同时运行所有任务
    await asyncio.gather(*waiters, setter)

if __name__ == "__main__":
    print("程序开始运行")
    asyncio.run(main())
    print("程序运行结束")

实际应用场景:

  • 资源初始化:多个任务等待某个资源(如数据库连接)初始化完成;
  • 任务协调:多个工作线程等待某个条件满足后开始工作;
  • 事件通知:实现发布-订阅模式的基础。

2.8 asyncio.Condition

asyncio.Condtion()常被用来需要等待某个资源条件是否满足的情况。

下面使用协程实现了一个生产者-消费者模式。

python 复制代码
import asyncio
import random
import time

# 共享资源
shared_resource = []
MAX_ITEMS = 5

async def producer(condition, name):
    """生产者协程:向共享资源添加数据"""
    while True:
        # 模拟生产数据的时间
        await asyncio.sleep(random.uniform(0.5, 1.5))
        
        async with condition:
            # 如果资源已满,等待消费者消费
            while len(shared_resource) >= MAX_ITEMS:
                print(f"生产者 {name}: 资源已满,等待消费者...")
                await condition.wait()
            
            # 生产数据
            item = f"产品-{name}-{time.time()}"
            shared_resource.append(item)
            print(f"生产者 {name} 生产了: {item}")
            print(f"当前资源数量: {len(shared_resource)}")
            
            # 通知所有等待的消费者
            condition.notify_all()

async def consumer(condition, name):
    """消费者协程:从共享资源消费数据"""
    while True:
        # 模拟消费数据的时间
        await asyncio.sleep(random.uniform(0.5, 1.5))
        
        async with condition:
            # 如果资源为空,等待生产者生产
            while len(shared_resource) == 0:
                print(f"消费者 {name}: 资源为空,等待生产者...")
                await condition.wait()
            
            # 消费数据
            item = shared_resource.pop(0)
            print(f"消费者 {name} 消费了: {item}")
            print(f"当前资源数量: {len(shared_resource)}")
            
            # 通知所有等待的生产者
            condition.notify_all()

async def main():
    # 创建条件对象
    condition = asyncio.Condition()
    
    # 创建生产者和消费者任务
    producers = [
        producer(condition, f"P{i}") for i in range(2)
    ]
    consumers = [
        consumer(condition, f"C{i}") for i in range(3)
    ]
    
    # 运行所有任务
    await asyncio.gather(*producers, *consumers)

if __name__ == "__main__":
    print("生产者-消费者模型开始运行")
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\n程序被用户中断")

执行结果:

你会发现 asyncio.Condtion()对象的 API·和 threading.Condition()很相似,这里就不再过多介绍了。

下面是这个程序的时序图,有助于你理解这个程序的逻辑。

使用场景:

  1. 生产者-消费者模式:协调生产者和消费者的工作,控制资源的使用和释放。
  2. 资源池管理:管理数据库连接池,控制线程池大小。
  3. 任务调度:实现任务队列,控制并发任务数量。
  4. 状态同步:等待特定状态变化,实现复杂的同步逻辑。
  5. 事件驱动系统:实现发布-订阅模式,处理异步事件通知。

asyncio.Event()的区别:

asyncio.Event()只有简单的标志位、只有设置和未设置两种状态、适合简单的同步场景。

asyncio.Condition()可以等待特定条件、支持选择性通知,比较适合复杂的同步场景。

2.9 第三方库

  • aiohttp: 异步HTTP客户端和服务器。
  • aiofiles: 异步文件处理。
  • asyncio_redis : 异步Redis客户端。
    示例(aiohttp):
python 复制代码
import aiohttp
import asyncio
async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()
async def main():
    async with aiohttp.ClientSession() as session:
        html = await fetch(session, 'https://example.com')
        print(html)
asyncio.run(main())

3. Python 协程的实现原理

3.1 协程和线程的区别

3.2 Python 协程的底层工作过程

Python协程的底层实现主要基于生成器和 yield 语句的扩展,以及通过事件循环来管理和调度这些协程,后来又引入了async/await语法。

下面的时序图展示了Python协程的底层工作过程,从协程创建到执行、挂起、恢复,直到完成的整个生命周期。

以下是图中关键步骤的详细解释:

  1. 创建阶段 :应用程序创建事件循环和协程对象。
    • 使用async def定义协程函数,创建事件循环
    • 协程在定义时并不执行,只是创建了一个协程对象
  2. 初始执行
    • 协程首次被事件循环执行时,会创建栈帧对象保存执行状态
    • 协程内部代码开始执行,直到遇到第一个await
  3. 挂起与调度
    • 当遇到await表达式时,协程会暂停执行并让出控制权
    • 底层通过yield机制实现,协程状态从GEN_RUNNING变为GEN_SUSPENDED
    • 被挂起的协程的栈帧会被保存,包括局部变量、指令指针等执行状态
  4. I/O操作与回调
    • 事件循环注册I/O回调,继续执行其他就绪的任务
    • 当I/O操作完成时,回调被触发,协程被标记为可恢复执行。 在 Python异步编程中,I/O操作完成通知事件循环的机制主要基于操作系统的I/O多路复用功能。I/O 多路复用机制,如 Linux 的 epoll、Windows 的 IOCP,这些机制允许一个进程监控多个文件描述符,等待它们变为可读或可写的状态。
  5. 恢复执行
    • 事件循环通过.send()方法恢复协程执行,传递I/O操作的结果
    • 协程从上次挂起的位置继续执行,状态从GEN_SUSPENDED变为GEN_RUNNING
  6. 完成或再次挂起
    • 如果协程执行完成,它会将结果返回给事件循环,状态变为GEN_COMPLETED
    • 如果遇到新的await点,协程会再次挂起,重复上述过程
相关推荐
五岁小孩吖2 分钟前
使用 decimal 包解决 go float 浮点数运算失真
后端
开心猴爷3 分钟前
iOS混淆工具使用,后续维护与版本升级中实用的混淆策略
后端
瘦的可以下饭了14 分钟前
python(列表、元组、字典、集合)
python
天才测试猿21 分钟前
什么是单元测试?
自动化测试·软件测试·python·测试工具·职场和发展·单元测试·测试用例
BeerBear31 分钟前
你对Code Review的看法是什么?
后端·面试·代码规范
站大爷IP32 分钟前
Python自定义异常:从入门到实践的轻松指南
python
尼丝34 分钟前
Token是如何保证安全不被篡改
前端·后端
要努力赚钱1 小时前
抱着 GPU 取暖:大模型训练那些高阶玩法
后端
树獭叔叔1 小时前
Node.js 事件循环:单线程模型下的并发魔法
后端·node.js
程序员小假1 小时前
我们来说一说 悲观锁、乐观锁、分布式锁的使用场景和使用技巧
后端