引言
Python 中的协程:
- 协程是一种轻量级的用户级线程,它在单线程内执行,不会阻塞主线程,可以在多个任务间轻松地切换,因此可以用于实现异步I/O操作。协程的实现方式与生成器非常相似,通过使用
yield
语句来暂停和恢复执行。 - 协程可以与
asyncio
库配合使用,来实现异步I/O操作。这种方式可以极大地提高程序的效率,因为程序不必等待I/O操作完成,可以在等待I/O操作期间执行其他任务。
协程框架基础
任何一个协程框架都首先必须是一个异步框架,asyncio也不例外。一个异步框架通常主要包括事件循环、事件队列、polling、timer队列,所有的异步框架皆不例外,asyncio也是如此。事件循环是实际启动之后执行的代码,事件队列用来向事件循环发送要执行的任务,polling使用multiplexing技术(如select或epoll)用来监控socket等IO活动,timer队列保存定时器,一般是个最小堆。
执行过程以asyncio为例,asyncio的事件队列里面就是普通的callable,callable执行的时候调用asyncio的其他接口配置polling或者timer队列或者发送更多callable到事件队列里。polling或者timer队列会将socket活动和计时器关联到不同的回调,这个回调不是立即执行的,而是延迟回调,也就是不直接调用而是放进事件队列里,等着事件循环去调用。事件循环(也就是所谓EventLoop)开始的时候,会不断从事件队列里取callable,然后一个一个call过去,call完换下一个;事件队列里没有了,就去timer队列里取一个最近的timer作为超时时间,去调用polling,直到任意socket活动,或者超时为止,然后根据不同的条件,将socket的回调放到事件队列里,从头开始执行。
那这么简单的结构怎么实现异步编程呢?原理也很简单,每个callable都是一小份工作,当这部分工作做完之后,会等待下一个条件:如果要等socket,就设置polling回调;如果要等超时时间,就设置timer;如果要等其他callable完成,就使用同步对象的callback接口(后面会讲的Future);如果要同时等多个条件,就把所有的回调都设置上。通过这些回调不断触发,就能实现异步编程的目的。
基于回调的代码结构在这件事情上就比较直白,直接将回调函数设置成polling或者timer或者同步对象的callback就行了。
asyncio协程模块
协程的代码基本构成通常包括以下几个关键元素:
- 创建协程对象:首先需要创建一个协程对象,这可以通过特定的语法或库函数来实现。在Python中,可以使用
async def
定义一个协程函数,或者使用asyncio.create_task()
创建一个协程任务。 - 执行协程:一旦创建了协程对象,就需要在适当的时机执行它。在Python中,可以通过
await
关键字来执行一个协程,将控制权交还给事件循环(event loop)。 - 暂停和恢复:协程的特点之一是可以在执行过程中暂停和恢复。在协程函数中,可以使用
await
关键字来暂停当前协程的执行,并等待其他协程或异步操作的完成。一旦等待的条件满足,协程将从暂停的地方继续执行。 - 异步操作:协程通常会涉及到异步操作,例如网络请求、文件读写等。这些异步操作可以使用特定的库函数或语法来完成,例如在Python中可以使用
await
关键字等待一个异步操作的结果。 - 协程的调度和并发:多个协程可以同时存在,并通过事件循环的调度来进行切换和执行。事件循环负责协程的调度和并发执行,可以根据需要进行协程的切换,从而实现并发执行的效果。
首先了解什么是协程函数
# 使用 async 声明的函数就是协程函数
async def fn():
pass
接下来我们再来看一下什么是协程对象
- 所谓的协程对象就是调用协程函数之后返回的对象如下代码所示:
# 使用 async 声明的函数就是协程函数
async def fn():
pass
# 调用协程函数得到的对象就是协程对象
res = fn()
print(res) # <coroutine object fn at 0x1029684a0>
- 注意事项:调用协程函数时,函数内部的代码不会执行,只是会返回一个协程对象!
最后了解什么是await关键字
- await 是一个只能在协程函数中使用的关键字,用于当协程函数遇到IO操作的时候挂起当前协程(任务),
- 当前协程挂起过程中,事件循环可以去执行其他的协程(任务)
- 当前协程IO处理完成时,可以再次切换回来执行 await 之后的代码
下面是一个简单的Python协程函数的例子:
import asyncio
async def my_coroutine():
print("Coroutine started")
await asyncio.sleep(1)
print("Coroutine resumed")
async def main():
print("Main program started")
task = asyncio.create_task(my_coroutine()) # 创建协程任务
await task
print("Main program finished")
asyncio.run(main()) # 执行主协程
在上面的例子中,my_coroutine
函数是一个协程函数,其中使用了await
语句来暂停和恢复协程的执行。main
函数是主协程函数,用于执行协程任务并管理协程的执行顺序。通过asyncio.create_task()
创建了一个协程任务,并通过await
等待任务的完成。最后使用asyncio.run()
来执行主协程。
这是一个简单的协程代码示例,实际的协程代码可能会更加复杂,涉及到更多的异步操作和协程的并发执行。
多任务的协程
在协程中实现多任务(即并发执行多个任务)是协程的一个重要应用场景。通过协程的暂停和恢复特性,可以在同一线程中执行多个协程任务,实现任务的协作和并发执行。
在协程中实现多任务的一种常见方式是使用事件循环(Event Loop),它负责协程的调度和执行。事件循环会不断地从可执行的协程队列中选择一个协程执行,直到所有协程完成或者被暂停。
下面是一个示例代码演示了如何使用事件循环实现多任务的协程:
import asyncio
async def task1():
print("Task 1 started")
await asyncio.sleep(1)
print("Task 1 completed")
async def task2():
print("Task 2 started")
await asyncio.sleep(2)
print("Task 2 completed")
async def main():
print("Main program started")
await asyncio.gather(task1(), task2()) # 并发执行两个任务
print("Main program finished")
asyncio.run(main()) # 运行主协程
在上面的代码中,task1()
和task2()
是两个协程函数,分别代表两个任务。main()
是主协程函数,在其中使用asyncio.gather()
函数来并发执行两个任务。await asyncio.gather(task1(), task2())
等待两个任务的完成,然后继续执行后续代码。
通过事件循环(由asyncio.run(main())
调用)以及asyncio.sleep()
函数的使用,可以在同一线程内并发执行多个协程任务。事件循环会根据任务的状态(是否被暂停或完成)来调度协程的执行。
需要注意的是,在协程中的阻塞操作(如IO操作)应该使用异步方式完成,以避免阻塞整个事件循环。常用的IO操作库如asyncio
和aiohttp
都提供了异步的IO操作支持。
通过使用事件循环和协程,可以方便地实现多任务的协作和并发执行,提高处理并发任务的效率和性能。
总结
协程是一种并发编程的技术,通过允许程序在执行过程中暂停和恢复,实现了高效利用资源、简化异步编程逻辑、提高代码可读性和可维护性、并发性能提升等优势。主要应用场景包括异步编程、事件驱动编程和并发任务调度等。
- 在程序中只要看到 async 和 await 关键字,其内部就是基于协程实现的异步编程
- 这种异步编程是通过一个线程在IO等待时间去执行其他任务,从而实现并发。