文章目录
- 协程简介
- 协程:像接力赛一样写程序
- 协程案例讲解
- Python中 async 的作用
- 如何理解协程中的事件循环?
- Python 代码示例:协程与事件循环
- ` await task1 await task2` 的作用是什么?
- 不是已经把他们都加入任务队列了吗,如果不加 `await task1` ,是不是 `await asyncio.sleep(1)` 执行完也会执行 `print("Task One finished")`?
- 那为什么在执行代码的时候注释掉 await task1 就不输出 print("Task One finished") 的内容了呢
- task1 = asyncio.create_task(task_one()) 这一句是否意味着把 task_one 加入任务队列了,调用 asyncio.run(main()) 就会执行 task_one
- asyncio.get_event_loop() 和 run_until_complete() 的作用
- Python 协程的执行顺序
协程简介
协程是一种并发编程模型,它允许我们在单个线程中执行多个任务,而无需使用多线程或多进程。与线程不同,协程是由程序员显式控制的,可以暂停和恢复执行,从而实现更精细的控制和更高的效率。
协程的特点:
- 轻量级: 协程比线程更轻量级,创建和销毁的成本更低,因为它们不依赖于操作系统内核的线程调度。
- 用户态调度: 协程的调度完全由程序员控制,可以根据需要暂停和恢复执行,从而实现更灵活的控制。
- 非阻塞式: 协程可以暂停执行并等待某个事件发生,而不会阻塞线程,从而提高程序的并发性能。
- 易于理解和使用: 协程通常使用类似于函数调用的语法,因此易于理解和使用。
协程的优势:
- 更高的并发性能: 协程可以在单个线程中执行多个任务,从而提高程序的并发性能。
- 更低的资源消耗: 协程比线程更轻量级,因此消耗更少的系统资源。
- 更简单的代码逻辑: 协程可以简化并发程序的代码逻辑,使其更容易理解和维护。
- 更高的代码可读性: 协程可以使用更自然的语法表达异步操作,从而提高代码可读性。
协程的应用场景:
- 异步 I/O 操作: 协程可以用于处理异步 I/O 操作,例如网络请求、文件读取等。
- 事件驱动编程: 协程可以用于实现事件驱动编程模型,例如 GUI 应用程序。
- 并发任务调度: 协程可以用于调度多个并发任务,例如爬虫、数据处理等。
协程与线程的比较:
特性 | 协程 | 线程 |
---|---|---|
调度方式 | 用户态调度 | 内核态调度 |
资源消耗 | 轻量级 | 重量级 |
控制方式 | 显式控制 | 隐式控制 |
并发性能 | 更高 | 更低 |
代码复杂度 | 更低 | 更高 |
需要注意的是, 协程并非适用于所有场景。例如,对于 CPU 密集型任务,使用多线程可能比协程更有效。
协程:像接力赛一样写程序
想象一下,你正在组织一场接力赛。每个队员负责跑一段路程,然后把接力棒交给下一位队员。队员们轮流跑,最终完成整个比赛。
协程就像这场接力赛:
- 每个队员就像一个"协程": 负责执行一部分代码,就像队员跑一段路程。
- 接力棒就像程序的执行权: 谁拿着接力棒,谁就负责跑。
- 队员之间传递接力棒: 就像协程之间互相传递执行权,你跑一段,我跑一段。
协程的优势:
- 更高效: 就像接力赛,队员们轮流跑,不会浪费时间干等,效率更高。
- 更灵活: 就像你可以根据队员的强项安排路程,协程也可以根据任务需要灵活安排代码执行顺序。
- 更省力: 就像你不需要每个队员都跑完全程,协程可以只执行必要的部分,节省资源。
举个例子:
你要做三件事: 下载文件、处理数据、显示结果。
- 传统程序: 像马拉松,必须依次完成,下载完才能处理,处理完才能显示,很慢。
- 多线程: 像多个队员同时跑,但需要协调步伐,容易出错,管理也麻烦。
- 协程: 像接力赛,下载一点就交给处理数据的队员,处理一点就交给显示结果的队员,流畅高效。
总结:
协程就是在一个线程里面执行多个任务,当某个任务发生阻塞时就切换到另一个任务。
协程案例讲解
async def func():
print("周杰伦")
if __name__ == "__main__":
f = func()
print(f) # <coroutine object func at 0x000001E1D3D3A040> 协程对象
Python中 async 的作用
在Python中,async
关键字用于定义一个协程(coroutine),这是一种特殊类型的函数,它允许你异步地执行代码。使用async
关键字的函数被称为异步函数(async function)。当你调用一个异步函数时,它不会立即执行,而是返回一个协程对象。这个协程对象需要被一个事件循环(event loop)驱动,以异步地执行其内部的代码。
下面是async
关键字的一些关键点:
- 定义异步函数:使用
async def
来定义一个异步函数。
async def my_async_function():
# 异步代码
- 协程对象:当调用异步函数时,它返回一个协程对象。
my_coro = my_async_function()
- 事件循环:要执行协程中的代码,需要将其传递给事件循环。
import asyncio
asyncio.run(my_coro)
- 异步等待:使用
await
关键字来挂起当前协程的执行,等待另一个协程完成。
async def another_async_function():
await my_async_function()
- 非阻塞IO:异步编程通常用于处理非阻塞I/O操作,如网络请求或文件读写。
- 并发执行:异步函数可以并发执行,而不需要多线程或多进程。
- 错误处理:异步函数中的异常需要使用
try...except
块来捕获。
async def my_async_function():
try:
# 可能引发异常的异步操作
except Exception as e:
# 异常处理
- 异步上下文管理器:可以使用
async with
和async for
来异步地处理资源。
async with async_resource as resource:
# 使用异步资源
- Python 3.5及以上版本:
async
和await
关键字是在Python 3.5中引入的。
使用async
和await
可以提高I/O密集型程序的性能,因为它们允许程序在等待I/O操作完成时执行其他任务。这使得Python程序能够更有效地处理并发I/O操作,特别是在网络编程和异步Web开发中。
如何理解协程中的事件循环?
理解一:
理解协程中的事件循环,可以从以下几个方面入手:
- 协程与线程的区别
- 线程: 操作系统级别的概念,由操作系统调度执行,占用系统资源较多。
- 协程: 用户态的轻量级线程,由编程语言的运行时调度执行,占用资源更少,创建和切换成本更低。
- 事件循环的作用
事件循环是协程运行时的核心机制,它负责:
- 维护一个任务队列: 存放待执行的协程任务。
- 不断循环检查队列: 当有可执行的任务时,取出并执行。
- 处理 I/O 事件: 当 I/O 操作完成时,将相应的协程任务添加到队列中。
- 协程与事件循环的协作
协程在执行过程中,如果遇到 I/O 操作,会主动 yield (让出) 控制权给事件循环。事件循环将该协程挂起,并将 I/O 操作注册到事件监听器中。当 I/O 操作完成后,事件监听器会通知事件循环,事件循环将该协程重新加入到任务队列中,等待执行。 - 事件循环的优势
- 非阻塞 I/O: 当协程执行 I/O 操作时,不会阻塞事件循环,其他协程可以继续执行。
- 高效的并发: 利用单线程实现高并发,避免多线程的资源竞争和上下文切换成本。
- 轻量级: 协程的创建和切换成本远低于线程。
- 理解的关键
- 协程是用户态的轻量级线程,由事件循环调度执行。
- 事件循环负责维护任务队列和处理 I/O 事件。
- 协程与事件循环协作,实现非阻塞 I/O 和高效并发。
举例说明:
假设有两个协程 A 和 B,都需要进行网络请求。
- 协程 A 先执行,发起网络请求后 yield 控制权给事件循环。
- 事件循环将协程 A 挂起,并将网络请求注册到事件监听器中。
- 协程 B 开始执行,也发起网络请求后 yield 控制权给事件循环。
- 事件循环将协程 B 挂起,并将网络请求注册到事件监听器中。
- 当协程 A 的网络请求完成后,事件监听器通知事件循环,事件循环将协程 A 加入任务队列。
- 当协程 B 的网络请求完成后,事件监听器通知事件循环,事件循环将协程 B 加入任务队列。
- 事件循环继续循环,取出队列中的协程并执行,直到所有任务完成。
在这个过程中,两个协程的网络请求是并发执行的,但整个过程只在一个线程内完成,避免了多线程的开销。
总结:
协程中的事件循环是实现高效异步编程的关键机制,通过协程主动 yield 和事件循环的调度,实现了非阻塞 I/O 和高效并发,使得程序能够在处理 I/O 密集型任务时保持高性能和响应速度。
理解二:
在Python中,事件循环(Event Loop)是异步编程的核心概念,特别是在使用asyncio
库进行协程编程时。事件循环是一个无限循环,它不断地检查和分发事件,驱动异步任务的执行。以下是对事件循环的进一步理解:
- 任务调度:事件循环负责调度协程和处理它们的执行。当一个协程被
await
时,事件循环会将其挂起,并继续执行其他协程。 - 非阻塞I/O:事件循环允许程序在等待I/O操作(如网络请求、文件读写)时执行其他任务。当I/O操作完成时,事件循环会唤醒相应的协程继续执行。
- 并发执行:事件循环通过在不同任务之间快速切换,实现并发执行。这种并发是协作式的,即任务需要显式地交出控制权(通过
await
)。 - 事件驱动:事件循环是事件驱动的,它会响应各种事件,如I/O操作完成、定时器超时等。
- 单线程:尽管事件循环可以实现并发执行,但它本身是单线程的。这意味着所有的协程实际上是在同一个线程上执行的。
**asyncio.run()**
:这是启动事件循环的常用方法。它创建一个新的事件循环,运行给定的协程,直到完成或引发异常。**asyncio.get_event_loop()**
:获取当前线程的事件循环。如果该线程还没有事件循环,则创建一个新的。**asyncio.create_task()**
:将协程包装成任务(Task),并将其添加到事件循环的任务队列中。**await**
和**asyncio.sleep()**
:在协程中使用await asyncio.sleep(0)
可以让出控制权,允许事件循环执行其他任务。- 错误处理:事件循环可以捕获并处理协程中的错误。如果协程中抛出异常,而没有被捕获,事件循环会终止。
- 关闭事件循环:在所有任务完成后,事件循环会自动关闭。也可以使用
loop.close()
显式关闭事件循环。 - 嵌套事件循环:在某些情况下,可以在一个事件循环中嵌套另一个事件循环。但通常不推荐这样做,因为它可能导致难以调试的问题。
- 异步上下文管理器:事件循环支持
async with
和async for
,允许异步地管理资源和遍历。 - 信号处理:事件循环可以响应操作系统的信号,如SIGINT(Ctrl+C)。
- 性能:由于事件循环避免了多线程和多进程的开销,它在I/O密集型任务中通常比传统的多线程或多进程模型更高效。
理解事件循环的工作原理对于编写高效的异步Python程序至关重要。通过合理地使用协程和事件循环,可以充分利用异步I/O的优势,提高程序的性能和响应性。
Python 代码示例:协程与事件循环
以下是用 Python 的 asyncio 库实现的协程和事件循环示例,可以帮助理解它们的工作机制:
import asyncio
async def task_one():
print("Task One started")
await asyncio.sleep(1) # 模拟 I/O 操作,等待1秒
print("Task One finished")
async def task_two():
print("Task Two started")
await asyncio.sleep(2) # 模拟 I/O 操作,等待2秒
print("Task Two finished")
async def main():
# 创建任务
task1 = asyncio.create_task(task_one())
task2 = asyncio.create_task(task_two())
# 等待任务完成
await task1
await task2
# 获取事件循环并运行 main 函数
asyncio.run(main())
代码解释:
- async def :定义异步函数,也称为协程。
- await asyncio.sleep(1) : 模拟 I/O 操作,协程主动 yield 控制权,并等待 1 秒。 await 关键字只能在协程内部使用。
- asyncio.create_task() : 将协程包装成任务并加入事件循环的任务队列。
- asyncio.run(main()) : 获取事件循环并运行 main() 协程。
- 事件循环: 在后台运行,不断检查任务队列,当遇到 await 时,会暂停当前协程,将控制权交还给事件循环。当 await 后面的操作完成后,事件循环会将协程重新加入任务队列,等待执行。
执行结果:
Task One started
Task Two started
Task One finished
Task Two finished
如果注释掉 await task1 和 await task2的执行结果:
Task One started
Task Two started
分析:
- 两个任务 task_one() 和 task_two() 几乎同时开始执行。
- task_one() 执行 await asyncio.sleep(1) 后,控制权交还给事件循环,task_two() 继续执行。
- 1 秒后,task_one() 完成 await asyncio.sleep(1),被事件循环重新加入任务队列。
- 2 秒后,task_two() 完成 await asyncio.sleep(2),被事件循环重新加入任务队列。
- 事件循环继续执行任务队列中的任务,直到所有任务完成。
总结:
- 协程通过 await 将控制权交给事件循环,实现非阻塞等待。
- 事件循环负责调度协程的执行,实现高效的并发处理。
关于上面代码案例的深入理解
await task1 await task2
的作用是什么?
在 async def main() 协程中,await task1 和 await task2 的作用是 确保协程 main() 等待 task1 和 task2 这两个任务完成后才结束。
具体来说:
- await task1: 这行代码会暂停 main() 协程的执行,直到 task1 协程完成并返回结果。这意味着 main() 协程会等待 task_one() 函数执行完毕。
- await task2: 同理,这行代码会暂停 main() 协程的执行,直到 task2 协程完成并返回结果。这意味着 main() 协程会等待 task_two() 函数执行完毕。
如果不使用 await,main() 协程会立即结束,而不会等待 task1 和 task2 完成。这会导致 task_one() 和 task_two() 的执行结果可能无法被正确处理。
总而言之,await task1 和 await task2 确保了 main() 协程会在 task1 和 task2 这两个任务完成后才结束,从而保证了程序的正确性。
不是已经把他们都加入任务队列了吗,如果不加 await task1
,是不是 await asyncio.sleep(1)
执行完也会执行 print("Task One finished")
?
即使不加 await task1,await asyncio.sleep(1) 执行完后,的确会执行 print(“Task One finished”)。
这是因为 task1 和 task2 已经被加入到事件循环的任务队列中,它们会独立地在事件循环的调度下执行。 main() 协程是否等待它们完成,并不会影响它们自身的执行流程。
那么,await task1 和 await task2 的意义何在呢?
- 顺序控制: await 语句可以控制 main() 协程的执行顺序,确保在 task1 和 task2 完成后,才继续执行 main() 协程后续的代码。 这在某些场景下很重要,例如:
- main() 协程后续的代码需要依赖 task1 和 task2 的执行结果。
- 需要确保 task1 和 task2 的执行顺序。
- 异常处理: await 语句可以捕获 task1 和 task2 抛出的异常,并在 main() 协程中进行处理。 如果没有 await,task1 和 task2 抛出的异常可能会被忽略,导致程序出现不可预知的行为。
所以,虽然 await task1 和 await task2 不会影响 task1 和 task2 本身的执行,但它们在控制 main() 协程的执行流程、处理异常等方面仍然起着重要作用。
那为什么在执行代码的时候注释掉 await task1 就不输出 print(“Task One finished”) 的内容了呢
你观察到的现象很有意思,也确实有可能发生。这与事件循环的运行机制以及程序结束的条件有关。
事件循环的运行机制:
事件循环会持续运行,直到满足以下条件之一:
- 所有任务都执行完毕。
- 事件循环被显式关闭 (例如调用 loop.close())。
程序结束的条件:
asyncio.run(main()) 会创建一个新的事件循环,运行 main() 协程,并在 main() 协程结束后关闭事件循环。
注释掉 await task1 的影响:
- main() 协程不再等待 task1 完成: 当你注释掉 await task1 后, main() 协程不会再等待 task1 完成,它会立即执行到 await task2,然后等待 task2 完成。
- main() 协程提前结束: 由于 task2 完成得比 task1 快,main() 协程会在 task1 完成之前就执行完毕,并导致事件循环关闭。
- task1 被中断: 事件循环关闭后,所有未完成的任务,包括 task1,都会被强制停止,因此 print(“Task One finished”) 就没有机会被执行。
总结:
当你注释掉 await task1 后,main() 协程不再等待 task1 完成,而 main() 协程的结束会触发事件循环的关闭,导致 task1 被中断,最终 print(“Task One finished”) 没有被执行。
为了确保 task1 完成,你可以采取以下措施:
- 保留 await task1
- 在 main() 协程中加入 await asyncio.sleep(n),确保事件循环运行足够长的时间,让 task1 有机会完成。
- 使用其他方式显式等待 task1 完成,例如 asyncio.wait() 或 asyncio.gather()。
task1 = asyncio.create_task(task_one()) 这一句是否意味着把 task_one 加入任务队列了,调用 asyncio.run(main()) 就会执行 task_one
你说得对,但不完全对。
task1 = asyncio.create_task(task_one()) 这一句只是将 task_one() 包装成一个任务 task1,并加入到事件循环的任务队列中,并不会立即执行 task_one()。
task_one() 的实际执行时间取决于事件循环的调度。只有当事件循环从任务队列中取出 task1 并开始执行时,task_one() 才会真正开始运行。
调用 asyncio.run(main()) 会启动事件循环,事件循环会开始处理任务队列中的任务,包括 task1。
因此,task_one() 的执行是 asyncio.run(main()) 启动事件循环后,由事件循环调度执行的,而不是 asyncio.create_task() 这句代码直接导致的。
总结:
- asyncio.create_task() 只是创建任务并加入任务队列,不等于立即执行。
- asyncio.run(main()) 启动事件循环,事件循环会调度执行任务队列中的任务。
- task_one() 的执行是由事件循环调度决定的,而不是 asyncio.create_task() 直接导致的。
asyncio.get_event_loop() 和 run_until_complete() 的作用
- asyncio.get_event_loop()
- 作用: 获取当前线程的事件循环。
- 解释: 每个线程都有一个默认的事件循环,asyncio.get_event_loop() 用于获取这个事件循环。如果当前线程没有事件循环,则会自动创建一个新的事件循环。
- 用途: 用于获取事件循环,以便后续操作,例如:
- 将任务加入事件循环的任务队列:loop.create_task(coro)
- 在事件循环中运行协程: loop.run_until_complete(coro)
- 手动控制事件循环:loop.run_forever(),loop.stop()
- loop.run_until_complete(future)
- 作用: 运行事件循环,直到传入的 future 完成。
- 解释: future 通常是一个协程或一个 asyncio.Task 对象。loop.run_until_complete(future) 会启动事件循环,执行任务队列中的任务,直到 future 完成。
- 用途: 用于运行一个协程或任务,并等待其完成。
二者的区别
- asyncio.get_event_loop() 只是获取事件循环,并不会启动它。
- loop.run_until_complete(future) 会启动事件循环,并运行到 future 完成。
示例
import asyncio
async def my_coroutine():
await asyncio.sleep(1)
print("Coroutine finished")
loop = asyncio.get_event_loop() # 获取事件循环
task = loop.create_task(my_coroutine()) # 创建任务并加入任务队列
loop.run_until_complete(task) # 运行事件循环,直到任务完成
print("Event loop finished")
注意:
- asyncio.run(coro) 是 asyncio 提供的更高级的 API,它会自动创建事件循环,运行协程,并在协程结束后关闭事件循环。
- 在 asyncio 3.7 之后,推荐使用 asyncio.run(coro) 来运行协程,而不是手动获取和控制事件循环。
Python 协程的执行顺序
- 创建协程:
- 使用 async def 定义协程函数。
- 调用协程函数,返回一个协程对象。
- 加入事件循环:
- 使用 asyncio.create_task(coro) 将协程对象包装成任务,并加入事件循环的任务队列。
- 使用 asyncio.ensure_future(coro) 将协程对象包装成 future,并加入事件循环的任务队列。
- 事件循环调度:
- 事件循环会持续运行,不断检查任务队列。
- 当遇到一个可执行的任务时,事件循环会取出并执行该任务。
- 协程的执行流程:
- 当协程执行到 await 语句时,会暂停执行,并将控制权交还给事件循环。
- 事件循环会将该协程挂起,并监听 await 后面的操作 (例如 I/O 操作) 的完成事件。
- 当 await 后面的操作完成后,事件监听器会通知事件循环。
- 事件循环会将该协程重新加入到任务队列中,等待执行。
- 事件循环继续调度执行其他任务。
- 协程的结束:
- 当协程执行完毕后,会返回结果或抛出异常。
- 事件循环会将该协程从任务队列中移除。
总结:
Python 协程的执行顺序由事件循环调度决定,await 语句会暂停协程的执行,并交出控制权给事件循环。事件循环会根据 I/O 事件的完成情况,决定何时恢复协程的执行。
注意:
- 多个协程之间的执行顺序是不确定的,取决于事件循环的调度策略和 I/O 操作的完成时间。
- 协程的执行过程中,可能会被其他协程打断,并在稍后的时间点恢复执行。