Python协程与asyncio

理解Python中的协程,我们需从其底层原理开始,逐步深入。协程的核心在于控制流的非阻塞式管理,它允许在单一线程内实现并发处理,通过事件循环协作式多任务来提高效率。

生成器基础

生成器简介

协程的前身是生成器(Generator)。 生成器是Python的一种特殊函数,它允许在执行过程中暂停并保留状态,以便稍后从同一位置继续执行

生成器通过yield关键字实现,它在函数体内作为返回值使用,而不是return。当yield被执行时,生成器会暂停执行,并将当前状态(包括局部变量和执行位置)保存起来。 当再次调用next()或send()时,生成器会从上次暂停的地方继续执行。

 生成器的这一特性为协程提供了基础。

# 例:
def simple_generator():
    print("Starting")
    yield 1
    print("Continuing")
    yield 2

gen = simple_generator()
print(next(gen))  # 输出 "Starting" 和 1
print(next(gen))  # 输出 "Continuing" 和 2

上下文切换

生成器的上下文切换是通过Python解释器内部的实现来完成的。简化流程如下:

1、生成器创建

当调用一个包含yield的函数时,Python不会立即执行函数体,而是返回一个生成器对象。

2、第一次调用next()

当首次调用next(generator)或通过for循环迭代生成器时,解释器会开始执行生成器函数,直到遇到第一个yield语句。此时,生成器函数暂停执行,yield表达式的右侧值被返回给调用者,生成器的状态被保存下来

3、上下文切换

生成器的上下文包括局部变量、执行堆栈和程序计数器(指示执行位置)。当生成器暂停时,这些信息都被保存在生成器对象中。 当再次调用next()或send()时,Python解释器会恢复生成器的上下文,就像从未离开过yield一样。

4、send()方法

除了通过next()恢复执行,还可以使用generator.send(value)来传递一个值给生成器。当生成器暂停在yield表达式时,send()方法可以将传入的值设置为yield表达式的值,然后继续执行。

5、生成器结束

当生成器函数执行完毕或遇到return语句时,会引发StopIteration异常,表示生成器完成。 如果在yield之后没有更多的代码,或者return语句没有返回值,StopIteration的value属性为None。如果return语句有返回值,StopIteration的value属性将是该返回值。

生成器小结

生成器的上下文切换是Python解释器在内部自动处理的,程序员不需要直接操作。这种机制使得生成器能够有效地处理复杂的迭代逻辑,同时保持较低的内存开销,因为它只在需要时才计算值。在实现协程时,生成器扮演了关键角色,通过asyncio库将生成器转化为异步操作的协程。

协程

协程的定义

Python 3.5引入了asyncawait关键字,使得协程的定义和使用更加直观。一个使用async def定义的函数是一个协程函数,它返回的是一个协程对象,而不是直接执行

# 例:
async def coroutine_example():
    print("Coroutine started")
    await asyncio.sleep(1)  # 模拟异步操作
    print("Coroutine finished")

# 使用asyncio.run或事件循环来执行
# asyncio.run(coroutine_example())

异步I/O与事件循环

事件循环(Event Loop): 是协程执行的核心,它负责调度任务,监听和处理事件(如I/O完成事件)。

await关键字: 当遇到await关键字时,协程会暂停,将控制权交还给事件循环,直到等待的异步操作完成。

协程的执行流程

1、定义协程函数: 使用async def关键字定义一个协程函数。这个函数可以包含await表达式,用来挂起协程的执行。

2、创建协程对象: 调用协程函数不会立即执行函数体,而是返回一个协程对象。

# 例:
async def my_coroutine():
    # 协程体
    pass

coro_obj = my_coroutine()

3、启动协程: 协程对象需要被事件循环调度才能开始执行。这通常通过将协程对象转换为任务(Task) 并提交给事件循环完成

# 例:
loop = asyncio.get_event_loop()
task = asyncio.create_task(coro_obj)
loop.run_until_complete(task)

或者使用asyncio.run()函数(Python 3.7+):

asyncio.run(my_coroutine())

3、挂起与恢复

(1)当协程执行到await表达式时,它会暂停执行并将控制权返回给事件循环

(2)事件循环可以在此期间调度其他协程或处理其他任务

(3)当await等待的异步操作完成时,事件循环会恢复协程的执行

4、结束协程

(1)协程执行完毕后,会自动返回。如果协程函数没有return语句,返回值默认为None。

(2)如果协程是作为任务运行的,事件循环会检测到任务完成,并更新相关状态

5、错误处理: 如果协程中发生异常,事件循环可以捕获并处理这些异常,或者根据配置将异常传播给调用者。

整个流程的关键在于asyncio的事件循环,它负责调度协程的执行,确保在等待I/O或其他异步操作时,可以执行其他任务, 从而实现非阻塞的异步执行。

内部实现原理

1、协程对象

 实际上是一个可迭代的对象,但具有额外的调度信息,使得事件循环可以管理它的执行。要执行这个协程,需要使用事件循环,比如通过asyncio.create_task()或直接在事件循环中运行。

2、堆栈帧

堆栈帧是Python执行模型中的一个重要组成部分,它描述了函数调用的上下文,包括局部变量、调用参数、返回地址等。在同步执行中,每当函数调用发生时,一个新的堆栈帧会被压入调用栈;函数返回时,对应的堆栈帧被弹出。对于协程,情况稍微复杂一些,因为协程可以暂停执行(通过await),此时它的状态(包括局部变量)会被保存在堆栈帧中,直到被事件循环再次激活。

3、协程调度

协程调度指的是事件循环如何管理协程的执行流程。Python的asyncio库维护了一个事件循环,这个循环负责监控和调度所有的协程任务。当协程遇到await表达式时,它会暂停当前的执行上下文(保存堆栈帧的状态),释放CPU控制权给事件循环。事件循环会检查是否有其他就绪的协程(即不需要等待I/O或其他外部事件的协程),如果有,就会切换到那个协程继续执行。这一过程被称为上下文切换,使得多个协程可以在单一线程中并发执行,提高了效率。

简而言之,协程对象是异步操作的表示,堆栈帧存储了协程执行的上下文信息,而协程调度则是事件循环根据协程的状态 自动进行的控制流管理,使得异步操作能够高效、有序地执行。

进程、线程和协程

1、进程(Process)

(1)资源分配单位: 进程是操作系统分配资源的基本单位,每个进程都有自己独立的内存空间(称为地址空间),包括代码、数据、堆和栈等。

(2)安全性: 进程间的数据是隔离的,通过进程间通信(IPC)来交换信息,这提供了较高的数据安全性。

(3)开销: 创建和销毁进程的开销较大,上下文切换(从一个进程到另一个进程)涉及到内存映射、寄存器状态、文件描述符等的保存和恢复,因此开销也较高。

(4)并发性: 操作系统调度进程进行并发执行,但同一时刻只有一个进程在执行(除非在多核CPU上)。

(5)独立性: 进程有自己的生命周期,不受其他进程影响,即使一个进程崩溃,也不会直接影响其他进程。

2、线程(Thread)

(1)轻量级: 线程是进程内的执行单元,共享进程的内存空间,创建和销毁线程的开销比进程小。

(2)并发性: 同一进程内的多个线程可以并发执行,尤其是在多核处理器上,可以充分利用硬件资源。

(3)上下文切换: 线程间切换的开销比进程切换小,因为它们共享内存,只需要保存和恢复少量寄存器状态。

(4)共享资源: 线程间可以直接访问共享内存,但也带来了数据竞争和同步问题,需要使用锁、信号量等机制来保护共享数据。

(5)风险: 一个线程的崩溃可能会影响整个进程。

3、协程(Coroutine)

(1)用户级调度: 协程是由用户代码控制的,而不是操作系统。它们在单个线程内切换,不需要操作系统级别的上下文切换。

(2)轻量级: 协程的创建和切换开销非常小,因为它们不涉及操作系统级别的资源分配。

(3)无共享状态: 协程通常设计为非共享状态,避免了锁和同步机制,减少了竞态条件的风险。

(4)合作式调度: 协程的执行依赖于彼此的协作,一个协程必须通过yield或await显式地让出控制权,才能切换到另一个协程。

(5)并发性: 在单线程中,协程可以实现逻辑上的并发,但不是真正的并行执行,因为它们仍受制于CPU的一个核。

总结来说,进程提供了资源隔离和安全性,但开销大且并发受限;线程在同一个进程内提供了更高的并发性,但需要同步机制来保证数据安全;协程则提供了一种轻量级的并发模型,适合于I/O密集型任务,减少了上下文切换的开销,但需要程序员手动管理执行流程。

协程示例

import asyncio

async def loop100():
    for i in range(100):
        print("loop100:" + str(i))
        await asyncio.sleep(0.1)

async def loop10():
    for i in range(10):
        print("loop10:" + str(i))
        await asyncio.sleep(0.2)

async def main():
    await asyncio.gather(loop100(), loop10())

asyncio.run(main())

执行上述代码会发现,loop100和loop10是并发执行的。由于loop100的循环次数多于loop10,且每次循环的等待时间短于后者,因此在输出中,将看到loop100的打印信息穿插在loop10的打印信息之间,且整体上loop100的打印频率更高。

这种并发执行模式提高了程序处理I/O密集型任务的效率,而不会增加额外的线程或进程开销。

Python协程事件循环与Nodejs事件循环

Python的协程和Node.js的事件循环虽然都是用于处理异步编程,但它们的实现方式和使用场景有所不同。

Python协程与事件循环(asyncio)

1、协程(Coroutines)

(1)Python 3.5及以上版本引入了async和await关键字,用于定义和使用协程。

(2)协程是用户级别的轻量级线程,由程序员控制执行流程,通过await关键字挂起和恢复。

(3)asyncio库提供了事件循环(Event Loop)来调度协程的执行,它负责处理I/O事件和协程之间的切换。

2、事件循环(Event Loop)

(1)Python的事件循环基于asyncio库,它负责监听I/O事件,如网络套接字的读写准备就绪。

(2)当一个协程遇到await并等待某个异步操作时,事件循环会切换到其他协程,直到等待的操作完成。

(3)asyncio库还提供了create_task等方法来创建和管理协程任务。

Node.js事件循环与异步编程

1、事件驱动编程:

(1)Node.js 采用事件驱动模型,事件循环是其核心,处理异步I/O事件。

(2)异步操作通常通过回调函数、Promise 或 async/await 语法实现。

2、事件循环(Event Loop):

(1)Node.js的事件循环是基于libuv库实现的,libuv是一个跨平台的库,处理I/O复用、线程池等。

(2)事件循环分为多个阶段,如定时器、I/O完成、检查等,每个阶段处理特定类型的回调。

3、异步处理:

(1)Node.js的异步I/O操作不会阻塞事件循环,而是立即返回,等待事件完成后再执行回调。

(2)async/await语法在Node.js中也是基于Promise实现的,提供了更友好的异步编程模型。

对比分析

1、控制流

Python协程允许程序员控制执行流程,而Node.js的异步编程通常由事件驱动。Python的asyncio库提供了更多的控制级别,如Task对象可以被取消,而Node.js的异步操作一旦启动,通常难以中断

2、并发模型

Python协程在单线程中运行,但可以实现并发执行,而Node.js的事件循环也在单线程中,也可以处理并发的I/O操作。Node.js 通过工作线程(Worker Threads)支持CPU密集型任务的并行处理,而Python协程主要处理I/O密集型任务

3、错误处理

Python的asyncio库提供了try/except来处理协程中的错误,而Node.js的异步错误通常通过try/catch或Promise.catch来捕获。

4、复杂性

Python的协程相对简单,但需要理解async和await的工作原理。Node.js的异步编程更复杂,特别是回调函数的嵌套可能导致回调地狱,而Promise和async/await则缓解了这个问题。

作者:Clarify
链接:https://juejin.cn/post/7376073065333588005

  • 11
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值