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则缓解了这个问题。

相关文章
|
14天前
|
C语言 Python 容器
Python:从头创建 Asyncio (1)
Python:从头创建 Asyncio (1)
12 2
Python:从头创建 Asyncio (1)
|
1天前
|
开发者 Python
探索 Python 中的协程:从基本概念到实际应用
在现代编程中,异步处理变得越来越重要,Python 通过其内置的协程提供了强大的工具来简化这一过程。本文将深入探讨 Python 中的协程,从基本概念出发,逐步展示其实际应用,并通过具体代码示例帮助你掌握这种技术。
|
2天前
|
存储 对象存储 Python
Python|玩转 Asyncio 任务处理(1)
Python|玩转 Asyncio 任务处理(1)
14 5
|
6天前
|
数据挖掘 调度 开发者
Python并发编程的艺术:掌握线程、进程与协程的同步技巧
并发编程在Python中涵盖线程、进程和协程,用于优化IO操作和响应速度。`threading`模块支持线程,`multiprocessing`处理进程,而`asyncio`则用于协程。线程通过Lock和Condition Objects同步,进程使用Queue和Pipe通信。协程利用异步事件循环避免上下文切换。了解并发模型及同步技术是提升Python应用性能的关键。
27 5
|
8天前
|
调度 开发者 UED
探索Python中的异步编程:从回调到协程
【6月更文挑战第14天】本文深入探讨了Python异步编程的演变历程,从最初的回调函数到现代的协程模型。我们将通过具体示例,展示如何利用asyncio库提升程序的执行效率和响应能力。文章旨在为读者提供一个清晰的异步编程发展脉络,并指导他们如何在项目中实际应用这些技术。
|
9天前
|
API Python
Python:从头创建 Asyncio (2)
Python:从头创建 Asyncio (2)
7 0
|
7天前
|
Java 开发者 计算机视觉
探索Python中的并发编程:线程与协程
本文将深入探讨Python中的并发编程,重点比较线程和协程的工作机制、优缺点及其适用场景,帮助开发者在实际项目中做出更明智的选择。
|
1月前
|
网络协议 调度 开发者
python中gevent基于协程的并发编程模型详细介绍
`gevent`是Python的第三方库,提供基于协程的并发模型,适用于I/O密集型任务的高效异步编程。其核心是协程调度器,在单线程中轮流执行多个协程,通过非阻塞I/O实现高并发。主要特点包括协程调度、事件循环的I/O模型、同步/异步编程支持及易用性。示例代码展示了一个使用`gevent`实现的异步TCP服务器,当客户端连接时,服务器以协程方式处理请求,实现非阻塞通信。
21 0
|
1月前
|
数据采集 数据库 C++
python并发编程:并发编程中是选择多线程呢?还是多进程呢?还是多协程呢?
python并发编程:并发编程中是选择多线程呢?还是多进程呢?还是多协程呢?
31 0
|
1月前
|
安全 调度 Python
探索Python中的并发编程:协程与多线程的比较
本文将深入探讨Python中的并发编程技术,重点比较协程与多线程的特点和应用场景。通过对协程和多线程的原理解析,以及在实际项目中的应用案例分析,读者将能够更好地理解两种并发编程模型的异同,并在实践中选择合适的方案来提升Python程序的性能和效率。