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

文章目录

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

Python 的并发编程是指在同一时间段内处理多个任务的编程方式。

并发编程能显著提升程序的性能和响应能力,尤其适用于 I/O 密集型和 CPU 密集型任务。

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

1. 多线程(threading模块)

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

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

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

在编程里,一个线程就是程序执行的一条路径。

单线程程序就像一个只能一次做一件事的人,而多线程程序就像是有多个分身,能同时处理多个任务。

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

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

原理:

多线程适合 I/O 密集型任务,比如网络请求、文件读写等。

在执行 I/O 操作时,线程会进入阻塞状态,此时 CPU 可切换到其他线程继续执行,从而提高程序的整体效率。

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

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

python 复制代码
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 的例子:

python 复制代码
import threading

# 共享资源
counter = 0
# 创建锁对象
lock = threading.Lock()

# 定义一个函数,用于对共享资源进行操作
def increment():
    global counter
    for _ in range(100000):
        # 获取锁
        lock.acquire()
        try:
            counter += 1
        finally:
            # 释放锁
            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模块)

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

python 复制代码
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 进行进程间通信的例子:

python 复制代码
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:
            break
        print(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 关键字来定义异步函数和协程。

下面是一个简单的例子:

python 复制代码
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 密集型的高并发场景,但在使用时需要掌握其复杂的编程模型

相关推荐
SsummerC5 分钟前
【leetcode100】零钱兑换Ⅱ
数据结构·python·算法·leetcode·动态规划
卓怡学长16 分钟前
w304基于HTML5的民谣网站的设计与实现
java·前端·数据库·spring boot·spring·html5
YONG823_API23 分钟前
深度探究获取淘宝商品数据的途径|API接口|批量自动化采集商品数据
java·前端·自动化
一眼青苔29 分钟前
切割PDF使用python,库PyPDF2
服务器·python·pdf
冰^34 分钟前
MySQL VS SQL Server:优缺点全解析
数据库·数据仓库·redis·sql·mysql·json·数据库开发
yzhSWJ39 分钟前
Spring Boot中自定义404异常处理问题学习笔记
java·javascript
电商数据girl1 小时前
产品经理对于电商接口的梳理||电商接口文档梳理与接入
大数据·数据库·python·自动化·产品经理
盖世英雄酱581361 小时前
分布式ID所有生成方案
java·后端
敖云岚1 小时前
【AI】SpringAI 第五弹:接入千帆大模型
java·大数据·人工智能·spring boot·后端
桦说编程1 小时前
CompletableFuture典型错误 -- 代码出自某大厂
java·后端·响应式编程