一文搞定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关键字的一些关键点:

  1. 定义异步函数:使用async def来定义一个异步函数。
async def my_async_function():
    # 异步代码
  1. 协程对象:当调用异步函数时,它返回一个协程对象。
my_coro = my_async_function()
  1. 事件循环:要执行协程中的代码,需要将其传递给事件循环。
import asyncio

asyncio.run(my_coro)
  1. 异步等待:使用await关键字来挂起当前协程的执行,等待另一个协程完成。
async def another_async_function():
    await my_async_function()
  1. 非阻塞IO:异步编程通常用于处理非阻塞I/O操作,如网络请求或文件读写。
  2. 并发执行:异步函数可以并发执行,而不需要多线程或多进程。
  3. 错误处理:异步函数中的异常需要使用try...except块来捕获。
async def my_async_function():
    try:
        # 可能引发异常的异步操作
    except Exception as e:
        # 异常处理
  1. 异步上下文管理器:可以使用async withasync for来异步地处理资源。
async with async_resource as resource:
    # 使用异步资源
  1. Python 3.5及以上版本asyncawait关键字是在Python 3.5中引入的。

使用asyncawait可以提高I/O密集型程序的性能,因为它们允许程序在等待I/O操作完成时执行其他任务。这使得Python程序能够更有效地处理并发I/O操作,特别是在网络编程和异步Web开发中。

如何理解协程中的事件循环?

理解一:
理解协程中的事件循环,可以从以下几个方面入手:

  1. 协程与线程的区别
  • 线程: 操作系统级别的概念,由操作系统调度执行,占用系统资源较多。
  • 协程: 用户态的轻量级线程,由编程语言的运行时调度执行,占用资源更少,创建和切换成本更低。
  1. 事件循环的作用
    事件循环是协程运行时的核心机制,它负责:
  • 维护一个任务队列: 存放待执行的协程任务。
  • 不断循环检查队列: 当有可执行的任务时,取出并执行。
  • 处理 I/O 事件: 当 I/O 操作完成时,将相应的协程任务添加到队列中。
  1. 协程与事件循环的协作
    协程在执行过程中,如果遇到 I/O 操作,会主动 yield (让出) 控制权给事件循环。事件循环将该协程挂起,并将 I/O 操作注册到事件监听器中。当 I/O 操作完成后,事件监听器会通知事件循环,事件循环将该协程重新加入到任务队列中,等待执行。
  2. 事件循环的优势
  • 非阻塞 I/O: 当协程执行 I/O 操作时,不会阻塞事件循环,其他协程可以继续执行。
  • 高效的并发: 利用单线程实现高并发,避免多线程的资源竞争和上下文切换成本。
  • 轻量级: 协程的创建和切换成本远低于线程。
  1. 理解的关键
  • 协程是用户态的轻量级线程,由事件循环调度执行。
  • 事件循环负责维护任务队列和处理 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库进行协程编程时。事件循环是一个无限循环,它不断地检查和分发事件,驱动异步任务的执行。以下是对事件循环的进一步理解:

  1. 任务调度:事件循环负责调度协程和处理它们的执行。当一个协程被await时,事件循环会将其挂起,并继续执行其他协程。
  2. 非阻塞I/O:事件循环允许程序在等待I/O操作(如网络请求、文件读写)时执行其他任务。当I/O操作完成时,事件循环会唤醒相应的协程继续执行。
  3. 并发执行:事件循环通过在不同任务之间快速切换,实现并发执行。这种并发是协作式的,即任务需要显式地交出控制权(通过await)。
  4. 事件驱动:事件循环是事件驱动的,它会响应各种事件,如I/O操作完成、定时器超时等。
  5. 单线程:尽管事件循环可以实现并发执行,但它本身是单线程的。这意味着所有的协程实际上是在同一个线程上执行的。
  6. **asyncio.run()**:这是启动事件循环的常用方法。它创建一个新的事件循环,运行给定的协程,直到完成或引发异常。
  7. **asyncio.get_event_loop()**:获取当前线程的事件循环。如果该线程还没有事件循环,则创建一个新的。
  8. **asyncio.create_task()**:将协程包装成任务(Task),并将其添加到事件循环的任务队列中。
  9. **await****asyncio.sleep()**:在协程中使用await asyncio.sleep(0)可以让出控制权,允许事件循环执行其他任务。
  10. 错误处理:事件循环可以捕获并处理协程中的错误。如果协程中抛出异常,而没有被捕获,事件循环会终止。
  11. 关闭事件循环:在所有任务完成后,事件循环会自动关闭。也可以使用loop.close()显式关闭事件循环。
  12. 嵌套事件循环:在某些情况下,可以在一个事件循环中嵌套另一个事件循环。但通常不推荐这样做,因为它可能导致难以调试的问题。
  13. 异步上下文管理器:事件循环支持async withasync for,允许异步地管理资源和遍历。
  14. 信号处理:事件循环可以响应操作系统的信号,如SIGINT(Ctrl+C)。
  15. 性能:由于事件循环避免了多线程和多进程的开销,它在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() 的作用
  1. asyncio.get_event_loop()
  • 作用: 获取当前线程的事件循环。
  • 解释: 每个线程都有一个默认的事件循环,asyncio.get_event_loop() 用于获取这个事件循环。如果当前线程没有事件循环,则会自动创建一个新的事件循环。
  • 用途: 用于获取事件循环,以便后续操作,例如:
    • 将任务加入事件循环的任务队列:loop.create_task(coro)
    • 在事件循环中运行协程: loop.run_until_complete(coro)
    • 手动控制事件循环:loop.run_forever(),loop.stop()
  1. 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 协程的执行顺序
  1. 创建协程:
  • 使用 async def 定义协程函数。
  • 调用协程函数,返回一个协程对象。
  1. 加入事件循环:
  • 使用 asyncio.create_task(coro) 将协程对象包装成任务,并加入事件循环的任务队列。
  • 使用 asyncio.ensure_future(coro) 将协程对象包装成 future,并加入事件循环的任务队列。
  1. 事件循环调度:
  • 事件循环会持续运行,不断检查任务队列。
  • 当遇到一个可执行的任务时,事件循环会取出并执行该任务。
  1. 协程的执行流程:
  • 当协程执行到 await 语句时,会暂停执行,并将控制权交还给事件循环。
  • 事件循环会将该协程挂起,并监听 await 后面的操作 (例如 I/O 操作) 的完成事件。
  • 当 await 后面的操作完成后,事件监听器会通知事件循环。
  • 事件循环会将该协程重新加入到任务队列中,等待执行。
  • 事件循环继续调度执行其他任务。
  1. 协程的结束:
  • 当协程执行完毕后,会返回结果或抛出异常。
  • 事件循环会将该协程从任务队列中移除。

总结:
Python 协程的执行顺序由事件循环调度决定,await 语句会暂停协程的执行,并交出控制权给事件循环。事件循环会根据 I/O 事件的完成情况,决定何时恢复协程的执行。
注意:

  • 多个协程之间的执行顺序是不确定的,取决于事件循环的调度策略和 I/O 操作的完成时间。
  • 协程的执行过程中,可能会被其他协程打断,并在稍后的时间点恢复执行。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值