Python协程与asyncio

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

理解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())

4、挂起与恢复

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

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

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


5、结束协程

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

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


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

整个流程的关键在于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则缓解了这个问题。

相关文章
|
1月前
|
Python
Python中的异步编程:使用asyncio和aiohttp实现高效网络请求
【10月更文挑战第34天】在Python的世界里,异步编程是提高效率的利器。本文将带你了解如何使用asyncio和aiohttp库来编写高效的网络请求代码。我们将通过一个简单的示例来展示如何利用这些工具来并发地处理多个网络请求,从而提高程序的整体性能。准备好让你的Python代码飞起来吧!
77 2
|
1月前
|
调度 开发者 Python
Python中的异步编程:理解asyncio库
在Python的世界里,异步编程是一种高效处理I/O密集型任务的方法。本文将深入探讨Python的asyncio库,它是实现异步编程的核心。我们将从asyncio的基本概念出发,逐步解析事件循环、协程、任务和期货的概念,并通过实例展示如何使用asyncio来编写异步代码。不同于传统的同步编程,异步编程能够让程序在等待I/O操作完成时释放资源去处理其他任务,从而提高程序的整体效率和响应速度。
|
1月前
|
API 调度 开发者
探索Python中的异步编程:从asyncio到Trio
本文将带你深入Python异步编程的心脏地带,从asyncio的基本概念到Trio的高级特性,我们将一起揭开Python异步编程的神秘面纱,并探讨它们如何改变我们的编程方式。
|
1月前
|
API 开发者 Python
探索Python中的异步编程:Asyncio与Tornado的对决
在这个快节奏的世界里,Python开发者面临着一个挑战:如何让代码跑得更快?本文将带你走进Python异步编程的两大阵营——Asyncio和Tornado,探讨它们如何帮助我们提升性能,以及在实际应用中如何选择。我们将通过一场虚拟的“对决”,比较这两个框架的性能和易用性,让你在异步编程的战场上做出明智的选择。
|
1月前
|
数据库 Python
异步编程不再难!Python asyncio库实战,让你的代码流畅如丝!
在编程中,随着应用复杂度的提升,对并发和异步处理的需求日益增长。Python的asyncio库通过async和await关键字,简化了异步编程,使其变得流畅高效。本文将通过实战示例,介绍异步编程的基本概念、如何使用asyncio编写异步代码以及处理多个异步任务的方法,帮助你掌握异步编程技巧,提高代码性能。
84 4
|
1月前
|
API 数据处理 Python
探秘Python并发新世界:asyncio库,让你的代码并发更优雅!
在Python编程中,随着网络应用和数据处理需求的增长,并发编程变得愈发重要。asyncio库作为Python 3.4及以上版本的标准库,以其简洁的API和强大的异步编程能力,成为提升性能和优化资源利用的关键工具。本文介绍了asyncio的基本概念、异步函数的定义与使用、并发控制和资源管理等核心功能,通过具体示例展示了如何高效地编写并发代码。
42 2
|
1月前
|
测试技术 Python
Python中的异步编程与`asyncio`库
Python中的异步编程与`asyncio`库
|
1月前
|
调度 开发者 Python
异步编程在Python中的应用:Asyncio和Coroutines
异步编程在Python中的应用:Asyncio和Coroutines
31 1
|
2月前
|
关系型数据库 MySQL 数据处理
探索Python中的异步编程:从asyncio到异步数据库操作
在这个快节奏的技术世界里,效率和性能是关键。本文将带你深入Python的异步编程世界,从基础的asyncio库开始,逐步探索到异步数据库操作的高级应用。我们将一起揭开异步编程的神秘面纱,探索它如何帮助我们提升应用程序的性能和响应速度。
|
2月前
|
调度 Python
python知识点100篇系列(20)-python协程与异步编程asyncio
【10月更文挑战第8天】协程(Coroutine)是一种用户态内的上下文切换技术,通过单线程实现代码块间的切换执行。Python中实现协程的方法包括yield、asyncio模块及async/await关键字。其中,async/await结合asyncio模块可更便捷地编写和管理协程,支持异步IO操作,提高程序并发性能。协程函数、协程对象、Task对象等是其核心概念。