当前位置: 首页 > news >正文

【Python语言基础】24、并发编程

文章目录

    • 1. 多线程(threading模块)
      • 1.1 多线程的实现(threading 模块)
      • 1.2 多线程的优缺点
      • 1.3 线程同步与锁
    • 2. 多进程(multiprocessing模块)
      • 2.1 多进程实现(multiprocessing模块)
      • 2.2 多进程的优缺点
      • 2.3 进程间通信(IPC)
    • 3.异步编程(asyncio模块)
      • 3.1 异步编程的实现(asyncio 模块)
      • 3.2 异步编程的优缺点
      • 3.3 异步编程中的重要概念
      • 3.4 异步编程的应用场景

Python 的并发编程是指在同一时间段内处理多个任务的编程方式。
并发编程能显著提升程序的性能和响应能力,尤其适用于 I/O 密集型和 CPU 密集型任务。

下面将详细介绍 Python 并发编程的几种常见方式。

1. 多线程(threading模块)

想象你在一家餐厅当服务员,你要同时为好几桌客人服务。

如果按单线程的方式,你得先为第一桌客人点完菜、上完菜、结完账,再去服务第二桌客人,这样效率会很低。

而多线程就像是你同时能照顾好几桌客人,在第一桌客人下单后等待厨房做菜的时间里,你可以去第二桌客人那里点单,等第一桌菜做好了再去上菜,这样就能在相同时间内服务更多客人,提高了整体的工作效率。

在编程里,一个线程就是程序执行的一条路径。
单线程程序就像一个只能一次做一件事的人,而多线程程序就像是有多个分身,能同时处理多个任务。

在 Python 中,多线程可以让程序在同一时间执行多个不同的代码块。

多线程是指在一个进程内创建多个线程,每个线程可以独立执行不同的任务。在 Python 里,threading模块可用于实现多线程编程。

原理:

多线程适合 I/O 密集型任务,比如网络请求、文件读写等。
在执行 I/O 操作时,线程会进入阻塞状态,此时 CPU 可切换到其他线程继续执行,从而提高程序的整体效率。

1.1 多线程的实现(threading 模块)

Python 提供了 threading 模块来实现多线程编程。下面我们通过一个简单的例子来看看如何使用多线程。

import threading
import time# 定义一个函数,模拟一个任务
def print_numbers():for i in range(5):print(f"Number {i}")time.sleep(1)  # 暂停 1 秒,模拟耗时操作# 定义另一个函数,模拟另一个任务
def print_letters():for letter in 'abcde':print(f"Letter {letter}")time.sleep(1)# 创建线程对象
thread1 = threading.Thread(target=print_numbers)
thread2 = threading.Thread(target=print_letters)# 启动线程
thread1.start()
thread2.start()# 等待线程执行完毕
thread1.join()
thread2.join()print("Both threads have finished.")

代码解释

  1. 导入模块:import threading 和 import time,threading 模块用于创建和管理线程,time 模块用于模拟耗时操作。
  2. 定义任务函数
    • print_numbers 函数会打印 0 到 4 的数字,每次打印后暂停 1 秒。
    • print_letters 函数会打印字母 ‘a’ 到 ‘e’,每次打印后也暂停 1 秒。
  3. 创建线程对象:使用 threading.Thread 类创建线程对象,target 参数指定线程要执行的函数。
  4. 启动线程:调用线程对象的 start 方法来启动线程。一旦调用 start 方法,线程就会开始执行指定的函数。
  5. 等待线程执行完毕:调用线程对象的 join 方法,它会阻塞当前线程,直到被调用的线程执行完毕。这样可以确保在主线程继续执行之前,所有子线程都已经完成任务。
  6. 主线程继续执行:当所有子线程都执行完毕后,主线程会继续执行,打印出 “Both threads have finished.”。

1.2 多线程的优缺点

优点

  • 提高效率:对于 I/O 密集型任务,比如网络请求、文件读写等,在等待 I/O 操作完成的时间里,CPU 可以去执行其他线程的任务,从而提高了程序的整体执行效率。
  • 响应性更好:在 GUI 程序中,使用多线程可以避免界面在执行耗时任务时出现卡顿,保证用户界面的流畅性。

缺点

  • 全局解释器锁(GIL):在 Python 中,由于 GIL 的存在,同一时刻只有一个线程可以执行 Python 字节码。这意味着对于 CPU 密集型任务,多线程并不能充分利用多核 CPU 的优势,反而可能因为线程切换的开销而导致性能下降。
  • 线程安全问题:当多个线程同时访问和修改共享资源时,可能会出现数据不一致的问题,比如多个线程同时对一个变量进行加 1 操作,可能会导致最终结果不正确。为了解决线程安全问题,需要使用锁机制(如 threading.Lock)来保证同一时刻只有一个线程可以访问共享资源。

1.3 线程同步与锁

为了避免多个线程同时访问和修改共享资源时出现问题,我们可以使用锁机制。

下面是一个使用 threading.Lock 的例子:

import threading# 共享资源
counter = 0
# 创建锁对象
lock = threading.Lock()# 定义一个函数,用于对共享资源进行操作
def increment():global counterfor _ in range(100000):# 获取锁lock.acquire()try:counter += 1finally:# 释放锁lock.release()# 创建线程对象
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=increment)# 启动线程
thread1.start()
thread2.start()# 等待线程执行完毕
thread1.join()
thread2.join()print(f"Final counter value: {counter}")

在这个例子中,我们使用 threading.Lock 来保证同一时刻只有一个线程可以对 counter 变量进行加 1 操作。lock.acquire() 用于获取锁,lock.release() 用于释放锁。使用 try…finally 语句可以确保即使在出现异常的情况下,锁也能被正确释放。

2. 多进程(multiprocessing模块)

为了更好地理解多进程,我们可以把计算机比作一个大型工厂。

在这个工厂里,每个车间就是一个进程,每个车间都有自己独立的设备、原材料和工作区域,它们可以同时进行不同的生产任务,彼此之间互不干扰。
在编程领域,进程是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位。

多进程编程就是让程序同时启动多个进程,每个进程都可以独立执行不同的任务,以此来提高程序的运行效率和处理能力。

多进程是指在操作系统中同时运行多个进程,每个进程有自己独立的内存空间和系统资源。

Python 的multiprocessing模块可用于实现多进程编程。

原理

多进程适合 CPU 密集型任务,例如大量的数值计算。由于 Python 的全局解释器锁(GIL),多线程在 CPU 密集型任务中无法充分利用多核 CPU 的优势,而多进程可以绕过 GIL 的限制,让每个进程在不同的 CPU 核心上并行执行。

2.1 多进程实现(multiprocessing模块)

下面通过一个简单的例子来展示如何使用多进程:

import multiprocessing
import time# 定义一个函数,模拟一个任务
def print_numbers():for i in range(5):print(f"Number {i}")time.sleep(1)# 定义另一个函数,模拟另一个任务
def print_letters():for letter in 'abcde':print(f"Letter {letter}")time.sleep(1)if __name__ == "__main__":# 创建进程对象process1 = multiprocessing.Process(target=print_numbers)process2 = multiprocessing.Process(target=print_letters)# 启动进程process1.start()process2.start()# 等待进程执行完毕process1.join()process2.join()print("Both processes have finished.")

代码解释

  1. 导入模块:import multiprocessing 和 import time,multiprocessing 模块用于创建和管理进程,time 模块用于模拟耗时操作。
  2. 定义任务函数
    • print_numbers 函数会打印 0 到 4 的数字,每次打印后暂停 1 秒。
    • print_letters 函数会打印字母 ‘a’ 到 ‘e’,每次打印后也暂停 1 秒。
  3. 创建进程对象:使用 multiprocessing.Process 类创建进程对象,target 参数指定进程要执行的函数。
  4. 启动进程:调用进程对象的 start 方法来启动进程。一旦调用 start 方法,进程就会开始执行指定的函数。
  5. 等待进程执行完毕:调用进程对象的 join 方法,它会阻塞当前进程,直到被调用的进程执行完毕。这样可以确保在主进程继续执行之前,所有子进程都已经完成任务。
  6. 主进程继续执行:当所有子进程都执行完毕后,主进程会继续执行,打印出 “Both processes have finished.”。

2.2 多进程的优缺点

优点

  • 充分利用多核 CPU:与 Python 多线程受全局解释器锁(GIL)的限制不同,多进程可以绕过 GIL 的限制,每个进程都可以在不同的 CPU 核心上并行执行,因此非常适合 CPU 密集型任务,例如大量的数值计算、图像处理等。
  • 稳定性高:由于每个进程都有自己独立的内存空间和系统资源,一个进程的崩溃不会影响其他进程的运行,提高了程序的稳定性。

缺点

  • 资源开销大:创建和销毁进程需要消耗较多的系统资源,包括内存和 CPU 时间。因此,在需要频繁创建和销毁进程的场景下,性能可能会受到影响。
  • 进程间通信复杂:由于每个进程都有自己独立的内存空间,进程之间不能直接共享数据,需要使用特定的方式进行通信,例如管道(Pipe)、队列(Queue)等,这增加了编程的复杂度。

2.3 进程间通信(IPC)

下面是一个使用 multiprocessing.Queue 进行进程间通信的例子:

import multiprocessing# 定义一个函数,用于向队列中添加数据
def producer(queue):for i in range(5):print(f"Producing {i}")queue.put(i)time.sleep(1)# 定义一个函数,用于从队列中取出数据
def consumer(queue):while True:item = queue.get()if item is None:breakprint(f"Consuming {item}")if __name__ == "__main__":# 创建队列对象queue = multiprocessing.Queue()# 创建进程对象producer_process = multiprocessing.Process(target=producer, args=(queue,))consumer_process = multiprocessing.Process(target=consumer, args=(queue,))# 启动进程producer_process.start()consumer_process.start()# 等待生产者进程执行完毕producer_process.join()# 向队列中放入 None,表示数据生产结束queue.put(None)# 等待消费者进程执行完毕consumer_process.join()print("All processes have finished.")

在这个例子中,producer 函数负责向队列中添加数据,consumer 函数负责从队列中取出数据。通过 multiprocessing.Queue 实现了进程间的数据传递。

3.异步编程(asyncio模块)

为了更好地理解异步编程,我们可以把它类比成日常生活中的场景。

假设你在餐厅用餐,当你点完菜后,服务员不会一直站在你桌前等菜做好,而是会去服务其他客人,等菜做好了再把菜端给你。这种在等待某个任务完成的同时可以去做其他事情的方式,就是异步的思想。

在编程中,异步编程是一种非阻塞的编程方式。
传统的同步编程中,程序会按照代码的顺序依次执行,当遇到一个耗时的操作(如网络请求、文件读写)时,程序会暂停执行,直到该操作完成。

而异步编程允许程序在等待这些耗时操作完成的同时,继续执行其他任务,从而提高程序的并发性能和响应能力。

3.1 异步编程的实现(asyncio 模块)

Python 提供了 asyncio 模块来支持异步编程,并且引入了 async 和 await 关键字来定义异步函数和协程。

下面是一个简单的例子:

import asyncio# 定义一个异步函数(协程)
async def task(name, delay):print(f"Task {name} started")await asyncio.sleep(delay)  # 模拟耗时操作print(f"Task {name} finished")# 定义主函数,也是一个协程
async def main():# 创建多个协程对象tasks = [task("A", 2),task("B", 1)]# 并发运行多个协程await asyncio.gather(*tasks)# 运行异步程序
asyncio.run(main())

代码解释

  1. 导入模块:import asyncio,asyncio 模块是 Python 实现异步编程的核心模块。
  2. 定义异步函数(协程):使用 async 关键字定义异步函数,异步函数也被称为协程。在上面的例子中,task 函数就是一个协程,它模拟了一个耗时的操作,通过 await asyncio.sleep(delay) 暂停执行一段时间。
  3. 定义主函数(协程):main 函数也是一个协程,它创建了多个协程对象,并使用 asyncio.gather 函数并发运行这些协程。asyncio.gather 会等待所有协程执行完毕后才会继续执行后续代码。
  4. 运行异步程序:使用 asyncio.run 函数来运行异步程序,它会自动创建事件循环并运行指定的协程。

3.2 异步编程的优缺点

优点

  • 高并发性能:异步编程非常适合 I/O 密集型任务,特别是高并发的网络请求场景。在等待 I/O 操作完成的时间里,程序可以继续执行其他任务,从而显著提高程序的并发性能。
  • 资源开销小:与多线程和多进程相比,异步编程不需要创建额外的线程或进程,因此资源开销更小,更适合处理大量的并发连接。

缺点

  • 编程模型复杂:异步编程的编程模型相对复杂,需要理解异步函数、协程、事件循环等概念。编写和调试异步代码时,可能会遇到一些难以排查的问题。
  • 兼容性问题:一些旧的库和代码可能不支持异步编程,需要进行额外的适配工作。

3.3 异步编程中的重要概念

  • 协程(Coroutine)
    协程是异步编程的核心概念之一,它是一种可以暂停和恢复执行的函数。
    在 Python 中,使用 async 关键字定义的函数就是协程函数,调用协程函数会返回一个协程对象。
    协程可以在执行过程中通过 await 关键字暂停执行,等待某个异步操作完成后再继续执行。

  • 事件循环(Event Loop)
    事件循环是异步编程的调度器,它负责管理和调度所有的协程。
    事件循环会不断地检查哪些协程已经准备好执行,然后依次执行这些协程。在 Python 中,asyncio 模块会自动创建和管理事件循环。

  • await 关键字
    await 关键字用于暂停协程的执行,等待一个可等待对象(如另一个协程、asyncio.Future 等)的结果。
    当遇到 await 时,协程会暂停执行,事件循环会去执行其他协程,直到等待的对象完成后,协程才会继续执行。

3.4 异步编程的应用场景

  • 网络爬虫:在爬取大量网页时,网络请求是一个典型的 I/O 密集型任务。使用异步编程可以在等待一个网页响应的同时,发起其他网页的请求,从而大大提高爬取效率。

  • Web 服务器:对于高并发的 Web 服务器,异步编程可以处理大量的客户端请求,避免线程或进程的创建和销毁带来的开销,提高服务器的性能和响应能力。

异步编程是一种强大的编程技术,尤其适用于 I/O 密集型的高并发场景,但在使用时需要掌握其复杂的编程模型

相关文章:

  • 单片机 + 图像处理芯片 + TFT彩屏 触摸滑动条控件
  • github 简单访问方法(无魔法)
  • YOLOv8 涨点新方案:SlideLoss FocalLoss 优化,小目标检测效果炸裂!
  • LeetCode算法题(Go语言实现)_60
  • 【python】一文掌握 markitdown 库的操作(用于将文件和办公文档转换为Markdown的Python工具)
  • 第1讲:Transformers 的崛起:从RNN到Self-Attention
  • 【AI提示词】艺人顾问
  • 实验三 进程间通信实验
  • Flink介绍——实时计算核心论文之Flink论文
  • 入门-C编程基础部分:19、输入 输出
  • nuxt3持久化存储全局变量
  • 深入浅出:Pinctrl与GPIO子系统详解
  • 模板偏特化 (Partial Specialization)
  • 开源漏洞扫描器:OpenVAS
  • Python函数与模块笔记
  • 【大模型实战】大模型推理加速框架 vllm 部署的方案
  • 使用String path = FileUtilTest.class.getResource(“/1.txt“).getPath(); 报找不到路径
  • 【Linux系统篇】:什么是信号以及信号是如何产生的---从基础到应用的全面解析
  • echart实现柱状图并实现柱子上方需要显示指定文字,以及悬浮出弹框信息,动态出现滚动条,动态更新x,y轴的坐标名称
  • linux sudo 命令介绍
  • 国家发改委党组在《人民日报》发表署名文章:新时代新征程民营经济发展前景广阔大有可为
  • 建设高标准农田主要目标是什么?有哪些安排?两部门有关负责人答问
  • “全国十大考古”揭晓:盘龙城遗址、周原遗址等入选
  • 远香湖畔“戏”味浓,“吾嘉有戏”探索戏剧与图书跨界融合
  • 特朗普激发加拿大爱国热情之下:大选提前投票人数创纪录,魁北克分离情绪被冲淡
  • 神二十6个半小时到站