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

相关文章
|
28天前
|
搜索推荐 程序员 调度
精通Python异步编程:利用Asyncio与Aiohttp构建高效网络应用
【10月更文挑战第5天】随着互联网技术的快速发展,用户对于网络应用的响应速度和服务质量提出了越来越高的要求。为了构建能够处理高并发请求、提供快速响应时间的应用程序,开发者们需要掌握高效的编程技术和框架。在Python语言中,`asyncio` 和 `aiohttp` 是两个非常强大的库,它们可以帮助我们编写出既简洁又高效的异步网络应用。
107 1
|
3天前
|
调度 开发者 Python
Python中的异步编程:理解asyncio库
在Python的世界里,异步编程是一种高效处理I/O密集型任务的方法。本文将深入探讨Python的asyncio库,它是实现异步编程的核心。我们将从asyncio的基本概念出发,逐步解析事件循环、协程、任务和期货的概念,并通过实例展示如何使用asyncio来编写异步代码。不同于传统的同步编程,异步编程能够让程序在等待I/O操作完成时释放资源去处理其他任务,从而提高程序的整体效率和响应速度。
|
10天前
|
调度 开发者 Python
异步编程在Python中的应用:Asyncio和Coroutines
异步编程在Python中的应用:Asyncio和Coroutines
12 1
|
12天前
|
关系型数据库 MySQL 数据处理
探索Python中的异步编程:从asyncio到异步数据库操作
在这个快节奏的技术世界里,效率和性能是关键。本文将带你深入Python的异步编程世界,从基础的asyncio库开始,逐步探索到异步数据库操作的高级应用。我们将一起揭开异步编程的神秘面纱,探索它如何帮助我们提升应用程序的性能和响应速度。
|
17天前
|
调度 Python
python知识点100篇系列(20)-python协程与异步编程asyncio
【10月更文挑战第8天】协程(Coroutine)是一种用户态内的上下文切换技术,通过单线程实现代码块间的切换执行。Python中实现协程的方法包括yield、asyncio模块及async/await关键字。其中,async/await结合asyncio模块可更便捷地编写和管理协程,支持异步IO操作,提高程序并发性能。协程函数、协程对象、Task对象等是其核心概念。
|
21天前
|
调度 开发者 Python
异步编程在Python中的应用:Asyncio和Coroutines
【10月更文挑战第12天】本文介绍了Python中的异步编程,重点讲解了`asyncio`模块和协程的概念、原理及使用方法。通过异步编程,程序可以在等待I/O操作时继续执行其他任务,提高整体效率。文章还提供了一个简单的HTTP服务器示例,展示了如何使用`asyncio`和协程编写高效的异步代码。
15 2
|
23天前
|
调度 开发者 Python
异步编程在Python中的应用:Asyncio和Coroutines
【10月更文挑战第10天】本文介绍了Python中异步编程的应用,重点讲解了`asyncio`模块和协程的概念、原理及使用方法。通过一个简单的HTTP服务器示例,展示了如何利用`asyncio`和协程实现高效的并发处理。
17 1
|
27天前
|
调度 开发者 Python
异步编程在Python中的应用:Asyncio和Coroutines
【10月更文挑战第6天】本文介绍了Python中的异步编程,重点讲解了`asyncio`模块和协程的概念、原理及使用方法。通过示例展示了如何利用`asyncio`和协程编写高效的异步代码,提高程序的性能和响应能力。
21 2
|
27天前
|
调度 开发者 Python
探索Python中的异步编程:从asyncio到Trio
在这个快节奏的技术世界里,Python的异步编程正变得越来越重要。本文将带你深入Python的异步编程世界,从asyncio的基础用法,到Trio的高级特性,我们将一探究竟。准备好,让我们一起揭开Python异步编程的神秘面纱。
|
7天前
|
NoSQL 关系型数据库 MySQL
python协程+异步总结!
本文介绍了Python中的协程、asyncio模块以及异步编程的相关知识。首先解释了协程的概念和实现方法,包括greenlet、yield关键字、asyncio装饰器和async/await关键字。接着详细讲解了协程的意义和应用场景,如提高IO密集型任务的性能。文章还介绍了事件循环、Task对象、Future对象等核心概念,并提供了多个实战案例,包括异步Redis、MySQL操作、FastAPI框架和异步爬虫。最后提到了uvloop作为asyncio的高性能替代方案。通过这些内容,读者可以全面了解和掌握Python中的异步编程技术。
24 0