※
在 Python 中,协程通常使用 async
和 await
关键字来定义和管理。通过使用这些关键字,你可以定义异步函数(async function)和异步上下文管理器(async context manager),这些函数和上下文管理器可以在需要时挂起(暂停)和恢复执行。这种方式可以让程序员编写类似于同步代码的异步程序,从而更容易地处理并发任务。但协程对于内核来说依旧是一个线程,并不是真正的并行运行。
协程的优点包括:
- 轻量级: 与线程相比,协程的创建和切换开销更小,因为它们在单个线程中运行。
- 简单: 使用
async
和await
关键字,可以更容易地编写并发代码,避免了传统多线程编程中的锁和同步问题。 - 高效: 在 I/O 密集型任务中,协程可以更好地利用系统资源,因为它们可以在等待 I/O 操作完成时挂起,而不会阻塞整个线程。
协程跟线程的区别:
线程是通过在内核态进行系统调度、切换、销毁的,而协程是在用户态,通过程序本身来进行调度
和切换,由于协程会将寄存器上下文和栈保存到其他地方,在切换回来时,会恢复之前保存的寄存器上
下文和栈。
因此协程在没有系统的参与下,整体性能会非常好,没有了线程之间切换的开销,可以获得很高的
运行效率; 同时由于不用线程切换,也就意味着协程不会出现同时访问同一个数据,造成线程安全的问
题,因此协程是安全的,不需要多线程的锁机制,所以基本上能用多线程的地方,也可以用协程,但在 Linux 内核中,单个线程通常只能在单个核心上执行。这是因为在默认情况下,Linux 内核使用的调度器(scheduler)通常会将一个线程限制在单个 CPU 核心上执行。这种限制有助于避免竞争条件和数据一致性问题。
如果想要利用多核性能,则需要做一定优化,比如使用多线程技术或者利用一些处理器提供的向量化指令集(如 Intel 的 SSE、AVX 指令集)进行优化,就像Archlinux的ALHP 软件源将 Arch Linux 官方源中的软件包进行了重新编译,使用了
x86-64-v2
和x86-64-v3
优化,这些优化就包含了向量化指令集的优化,可以提升一部分并行性能。
使用方法※
使用异步/协程 需要引入asyncio
关键字※
- async 放在函数前的声明,声明他是一个协程(异步)函数。
- await 用于表示此段逻辑需要进行挂起,await后面跟的一定是一个协程对象(可等待对象)。
可等待对象包括协程(async def定义的协程函数和其返回的对象)、任务(task)以及feature。
Future
是一种特殊的 低层级 可等待对象,表示一个异步操作的 最终结果。是async/await的底层对象,一般不建议在应用层直接使用。
定义一个协程函数※
import asyncio
async def func():
print('Hello!')
await dosomething() # 或者 await asyncio.sleep(1)
print('world')
启动协程※
注意:简单地调用一个协程并不会使其被调度执行
main()
<coroutine object main at 0x1053bb7c8>
- 使用生成器实现协程
def coroutine_example():
while True:
x = yield
print("Received:", x)
# 创建协程对象
coroutine = coroutine_example()
# 启动协程
next(coroutine)
# 发送数据给协程
coroutine.send("Hello")
- 使用 async/await 语法
通过 async/await 语法来声明协程是编写 asyncio 应用的推荐方式。
import asyncio
async def async_function():
print("Async function started")
await asyncio.sleep(1)
print("Async function completed")
# 运行异步函数
asyncio.run(async_function()) # asyncio.run() 函数用来运行最高层级的入口点 "main()" 函数
- 使用 asyncio 库提供的异步功能
import asyncio
async def async_task():
print("Async task started")
await asyncio.sleep(1)
print("Async task completed")
# 创建事件循环
loop = asyncio.get_event_loop()
# 运行异步任务
loop.run_until_complete(async_task())
# 关闭事件循环
loop.close()
- 并发运行多个协程即任务
## asyncio.create_task() 函数用来并发运行作为asyncio任务的多个协程。
async def main():
task1 = asyncio.create_task(
say_after(1, 'hello'))
task2 = asyncio.create_task(
say_after(2, 'world'))
print(f"started at {time.strftime('%X')}")
# Wait until both tasks are completed (should take
# around 2 seconds.)
await task1
await task2
print(f"finished at {time.strftime('%X')}")
创建任务※
asyncio.create_task(coro, *, name=None, context=None)
将 coro 协程 封装为一个 Task
并调度其执行。返回 Task 对象。
name 不为 None
,它将使用 Task.set_name()
来设为任务的名称。
可选的 context 参数允许指定自定义的 contextvars.Context
供 coro 运行。 当未提供 context 时将创建当前上下文的副本。
该任务会在 get_running_loop()
返回的循环中执行,如果当前线程没有在运行的循环则会引发 RuntimeError
。
备注
asyncio.TaskGroup.create_task() 是一个平衡了结构化并发的新选择;它允许等待一组相关任务并具有极强的安全保证。
重要
保存一个指向此函数的结果的引用,以避免任务在执行过程中消失。 事件循环将只保留对任务的弱引用。 未在其他地方被引用的任务可能在任何时候被作为垃圾回收,即使是在它被完成之前。 如果需要可靠的“发射后不用管”后台任务,请将它们放到一个多项集中:
background_tasks = set()
for i in range(10):
task = asyncio.create_task(some_coro(param=i))
# Add task to the set. This creates a strong reference.
background_tasks.add(task)
# To prevent keeping references to finished tasks forever,
# make each task remove its own reference from the set after
# completion:
task.add_done_callback(background_tasks.discard)
Added in version 3.7.
在 3.8 版本发生变更: 增加了 name 形参。
在 3.11 版本发生变更: 增加了 context 形参。