【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.")
代码解释
- 导入模块:import threading 和 import time,threading 模块用于创建和管理线程,time 模块用于模拟耗时操作。
- 定义任务函数:
- print_numbers 函数会打印 0 到 4 的数字,每次打印后暂停 1 秒。
- print_letters 函数会打印字母 ‘a’ 到 ‘e’,每次打印后也暂停 1 秒。
- 创建线程对象:使用 threading.Thread 类创建线程对象,target 参数指定线程要执行的函数。
- 启动线程:调用线程对象的 start 方法来启动线程。一旦调用 start 方法,线程就会开始执行指定的函数。
- 等待线程执行完毕:调用线程对象的 join 方法,它会阻塞当前线程,直到被调用的线程执行完毕。这样可以确保在主线程继续执行之前,所有子线程都已经完成任务。
- 主线程继续执行:当所有子线程都执行完毕后,主线程会继续执行,打印出 “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.")
代码解释
- 导入模块:import multiprocessing 和 import time,multiprocessing 模块用于创建和管理进程,time 模块用于模拟耗时操作。
- 定义任务函数:
- print_numbers 函数会打印 0 到 4 的数字,每次打印后暂停 1 秒。
- print_letters 函数会打印字母 ‘a’ 到 ‘e’,每次打印后也暂停 1 秒。
- 创建进程对象:使用 multiprocessing.Process 类创建进程对象,target 参数指定进程要执行的函数。
- 启动进程:调用进程对象的 start 方法来启动进程。一旦调用 start 方法,进程就会开始执行指定的函数。
- 等待进程执行完毕:调用进程对象的 join 方法,它会阻塞当前进程,直到被调用的进程执行完毕。这样可以确保在主进程继续执行之前,所有子进程都已经完成任务。
- 主进程继续执行:当所有子进程都执行完毕后,主进程会继续执行,打印出 “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())
代码解释
- 导入模块:import asyncio,asyncio 模块是 Python 实现异步编程的核心模块。
- 定义异步函数(协程):使用 async 关键字定义异步函数,异步函数也被称为协程。在上面的例子中,task 函数就是一个协程,它模拟了一个耗时的操作,通过 await asyncio.sleep(delay) 暂停执行一段时间。
- 定义主函数(协程):main 函数也是一个协程,它创建了多个协程对象,并使用 asyncio.gather 函数并发运行这些协程。asyncio.gather 会等待所有协程执行完毕后才会继续执行后续代码。
- 运行异步程序:使用 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 密集型的高并发场景,但在使用时需要掌握其复杂的编程模型