Python如何进行异步编程?

作者:禅与计算机程序设计艺术

1.简介

Python支持多种方式进行异步编程。其中最基础、最常用的方法就是线程池。本文将详细介绍Python中不同异步编程模型及其实现细节。并举例说明如何在生产环境中实践异步编程。

2. 什么是异步编程?

异步编程(Asynchronous Programming)是一种编程范式,它允许任务在没有被阻塞的情况下运行,并且可以同时执行多个任务。换句话说,异步编程让单个任务不必等待另一个任务结束,而是在不需要的时候才切换到另一个任务。

异步编程通常有两种主要形式:

  • 通过消息队列(Message Queueing)实现的事件驱动编程(Event Driven Programming)
  • 通过协程(Coroutines)实现的微线程编程(Micro Threads Programming)

Python对异步编程的支持包括以下三类库或模块:

  • asyncio(Python 3.4+):提供对Python异步编程的完整解决方案,其提供了用于创建高性能网络应用、长耗时IO任务等的工具
  • aiohttp(Python 3.4+):是一个基于asyncio模块的HTTP客户端/服务器框架,可用于编写异步Web应用
  • curio(Python 3.5+):是一个轻量级的基于协程的异步I/O库,类似于trio(Python 3.7+),提供了比asyncio更低的抽象级别

本文将重点介绍asyncio库的使用。

3. 线程 vs 进程

了解线程和进程之前,首先要理解计算机的处理机制。计算机系统由CPU、主存、硬盘组成。当计算机启动后,操作系统会从硬盘加载启动程序,将控制权交给它。启动程序将初始化一些必要的数据结构,如进程表、内存管理单元等。之后,CPU就开始工作了。

处理器通过读取程序指令、指令操作数以及数据并进行运算。当一条指令需要访问外部设备或需要暂停当前正在执行的任务时,就会发生中断。比如,用户按下键盘上的某个按钮,则系统会向CPU发出中断信号,通知处理器暂停当前正在执行的代码,转去执行其他任务。当处理完当前任务后,CPU又自动转回继续执行上一次中断的那条指令。

为了提高处理效率,现代计算机都采用了多任务处理技术。一个CPU可以同时执行多个任务,每条指令由不同的任务执行。这种多任务处理的方式被称作“多路复用”,即同一时刻 CPU 可以运行多个任务。每个任务都有自己的任务栈、程序计数器、寄存器等私有数据,互不干扰。这种处理方式使得 CPU 更加有效地利用时间,并提高了处理能力。

但同时存在的问题也很突出。由于每个任务都有自己的独立栈和寄存器等资源,因此需要管理好这些资源,避免多个任务之间出现冲突。另外,不同任务之间的切换也需要花费时间,导致系统整体的响应速度变慢。

为了解决上述问题,操作系统引入了进程这个概念。进程是操作系统对一个正在运行的程序的封装。每个进程都有自己独立的内存空间,可以执行自己的程序,且拥有自己的进程ID、父进程ID等信息。操作系统负责分配系统资源,如 CPU、内存、文件等,以便多个进程可以共享资源。

扫描二维码关注公众号,回复: 16977949 查看本文章

操作系统还引入了线程(Thread)这个概念。线程是比进程更小的执行单元。一个进程可以包含多个线程,每个线程执行不同的任务。相对于进程来说,线程之间共享内存空间、文件描述符等资源,因此线程间通信容易。线程是进程中的调度单位,由内核负责调度和切换。

总结一下,进程是系统资源分配和调度的最小单位,线程是CPU调度的最小单位。线程优于进程的一个重要原因是,它们共享内存和全局变量,使得进程切换时间减少。但是,也存在着一些缺点,例如线程切换开销较大、资源消耗大等。因此,一般情况下,多线程编程还是推荐使用多进程编程。

4. 为何使用异步编程?

因为多线程编程的复杂性、频繁上下文切换和线程安全问题,所以很多程序员宁愿选择使用进程。但是,对于那些要求高度实时的程序(比如,音视频播放器),使用多线程反而更合适。

由于多线程的同步问题,多线程编程往往需要花费更多的时间精力来维护线程之间的通信。而且,当某个线程发生异常时,整个进程都会受影响,造成难以排查的bug。如果能够把复杂的业务逻辑分布到多个线程中,各自完成特定任务,那么程序就可以充分利用CPU资源,提高处理能力。异步编程正是为了解决这一问题。

除了实时的程序外,异步编程还可以用来处理回调函数嵌套过深的问题,提升程序的响应速度。

5. asyncio 模块

Python 3.4版本引入了asyncio模块,它是Python异步编程的基础。asyncio模块提供了对异步编程的完整支持。

asyncio模块提供了一个高层次的接口,可以让我们方便地构建基于事件循环的应用。asyncio中最重要的是两个概念——事件循环和Tasks。

事件循环

事件循环(event loop)是一个运行在asyncio应用中的一个特殊的线程,它不断地监听事件(比如I/O请求、定时事件或者子进程退出)并执行相应的回调函数。事件循环是asyncio的核心,它负责处理所有的任务和回调函数。

事件循环的实现可以使用asyncio模块提供的loop()或者run_until_complete()方法。loop()方法直接在当前线程上运行事件循环,而run_until_complete()方法则可以在新线程或进程中运行事件循环,从而实现异步操作。

Tasks

Task是asyncio的执行单元。当我们调用asyncio的函数或方法时,返回值通常是一个Task对象。Task代表一个待完成的coroutine,它可能是已经完成的,也可能是尚未开始的。通过Task,我们可以实现多个协程的并发执行。

Task对象可用于跟踪子任务的进度和状态。例如,我们可以把多个协程并发地执行,然后等待它们的完成,通过检查每个协程是否已经完成来判断是否结束。

概念总结

综上所述,asyncio模块有如下几点重要特性:

  • 提供了异步编程的全面解决方案
  • 使用事件循环模型,所有任务都由事件循环来处理
  • 支持多种异步模型,包括常规函数、回调函数和协程
  • Task可以跟踪子任务的进度和状态,方便异步操作的管理
  • 提供了loop()和run_until_complete()方法,可以方便地在当前线程和其他线程中运行事件循环

6. 简单示例

假设我们有一个计算求积分的程序,它包含三个子任务:

  • 子任务1: 生成一个数字列表
  • 子任务2: 对数字列表进行累加求和
  • 子任务3: 用PI值计算积分结果

这三个子任务可以分解为两步:

  1. 生成一个数字列表
  2. 根据数字列表和PI值,计算积分结果

异步编程模型只需关注一步即可。但考虑到整个过程涉及到IO操作(生成数字列表),所以不能简单的通过多线程的方式来并行处理。因此,我们可以先采用异步编程模型编写第二步,然后再在最后一步中等待IO操作完成。

如下图所示,我们通过异步编程模型编写该程序。

async def compute_integral(): # 子任务1: 生成数字列表 num_list = await generate_num_list()

# 子任务2: 累加求和
total = sum(num_list)

# 子任务3: 用PI值计算积分结果
integral = total * math.pi

return integral

async def main(): start_time = time.monotonic() result = await compute_integral() end_time = time.monotonic()

print("Integral of x from 0 to pi is:", result)
print("Time taken:", end_time - start_time, "seconds")

if name == "main": import asyncio import random import time import math

asyncio.run(main())

这里,我们定义了两个异步函数compute_integral()和main()。compute_integral()函数负责计算积分结果,它会调用generate_num_list()函数生成数字列表。

generate_num_list()函数是一个普通的同步函数,用于生成随机数字列表。

main()函数是我们的入口函数,它通过asyncio.run()函数启动事件循环,并运行compute_integral()函数。

这样,我们就通过异步编程模型,解决了程序中计算积分结果所需的所有子任务。

猜你喜欢

转载自blog.csdn.net/universsky2015/article/details/133504571
今日推荐