六、Python 多任务编程:进程、线程
目录
在 Python 开发中,面对大量 I/O 操作、高并发请求等场景,单任务执行的效率短板会被无限放大。而多任务编程能让程序在同一时间处理多个操作,充分利用 CPU 资源,是提升程序执行效率的核心手段。Python 中实现多任务的三大核心方式为进程、线程和协程,三者各有特性和适用场景。
一、多任务基础:并发与并行
想要理解进程、线程和协程,首先要明确多任务的两种核心表现形式:并发和并行,这是多任务编程的底层逻辑。
- 并发:在一段时间内交替执行多个任务,适用于单核 CPU。操作系统会让各个任务轮流执行极短的时间,由于 CPU 执行速度极快,从表面看就像任务在同时运行,本质是任务数大于 CPU 核心数。
- 并行:在一段时间内真正同时执行多个任务,适用于多核 CPU。操作系统会为每个 CPU 内核分配独立的任务,多个任务在不同内核上同时运行,本质是任务数小于或等于 CPU 核心数。
简单来说,并发是 “交替做”,并行是 “同时做”,而进程、线程、协程的设计,正是为了在不同场景下实现高效的并发或并行。
二、进程:操作系统资源分配的最小单位
2.1、进程的核心概念
进程(Process)是操作系统进行资源分配和调度的基本单位,一个正在运行的程序就是一个进程(比如运行中的 QQ、Python 解释器)。一个程序运行后至少会创建一个主进程,若需要实现多任务,可由主进程创建多个子进程,每个子进程独立执行一个任务。
进程的核心特点:每个进程拥有独立的内存空间和系统资源,进程之间相互独立,互不干扰。
2.2、多进程的实现
Python 中通过multiprocessing模块实现多进程,核心步骤为导入模块、创建进程对象、启动进程,同时支持为进程任务传递参数(元组args/ 字典kwarg s)。
# 导入 多进程模块 import multiprocessing import time # 定义任务函数 def music(num, name): for i in range(num): print(f"听{name}...{i+1}") time.sleep(0.2) def coding(count): for i in range(count): print(f"敲代码...{i+1}") time.sleep(0.2) if __name__ == '__main__': # 创建进程对象,传递参数 target:函数名 p1 = multiprocessing.Process(target=music, args=(3, "忘情水")) p2 = multiprocessing.Process(target=coding, kwargs={"count": 3}) # 启动进程 p1.start() p2.start() # 等待子进程执行完毕后,主进程再继续 # join:让主进程阻塞,让子进程执行完毕后再执行其它进程 p1.join() p2.join() print("主进程结束")
运行这段多进程代码时,每次输出的顺序可能不一致(比如先打印 “听忘情水” 还是先打印 “敲代码” 的顺序不固定),核心原因是:操作系统对进程的调度是随机的、抢占式的,而非按代码编写顺序执行。
2.3、进程的关键知识点
进程编号
每个进程有唯一的 PID(进程 ID),可通过os.getpid( )获取当前进程 ID,os.getppid( )获取父进程 ID,用于进程管理。
import multiprocessing import os import time def child_task(): # 子进程中获取自身PID和父进程PID print(f"子进程 - 自身PID: {os.getpid()}, 父进程PID: {os.getppid()}") time.sleep(1) # 防止子进程过快结束 if __name__ == '__main__': # 主进程PID main_pid = os.getpid() print(f"主进程 - 自身PID: {main_pid}") # 创建2个子进程 p1 = multiprocessing.Process(target=child_task) p2 = multiprocessing.Process(target=child_task) p1.start() p2.start() p1.join() p2.join() print("所有子进程执行完毕")
进程间不共享全局变量
创建子进程时,会拷贝主进程的所有资源,子进程操作的是自己的资源副本,而非主进程的全局变量,因此进程间无法直接共享数据。
import multiprocessing import time # 定义全局变量 global_num = 0 def modify_num(): global global_num # 声明使用全局变量(实际是子进程的副本) global_num += 10 print(f"子进程内修改后:global_num = {global_num}") if __name__ == '__main__': print(f"主进程初始:global_num = {global_num}") # 创建子进程并执行修改操作 p = multiprocessing.Process(target=modify_num) p.start() p.join() # 主进程查看全局变量(未被修改) print(f"主进程最终:global_num = {global_num}")
主进程与子进程的结束顺序
主进程默认不会等待子进程执行完毕,若需主进程结束时子进程也立即停止,有两种方案:
- 设置守护进程:p1.daemon = True,主进程结束,子进程直接销毁;
- 主动销毁子进程:p1.terminate( ),强制终止子进程。
import multiprocessing import time def long_task(name): print(f"子进程{name}启动,开始执行长时间任务...") i = 0 while True: # 无限循环,模拟长时间任务 i += 1 print(f"子进程{name}运行中...{i}") time.sleep(0.5) if __name__ == '__main__': # ========== 案例1:设置守护进程 ========== print("===== 测试守护进程 =====") p_daemon = multiprocessing.Process(target=long_task, args=("守护进程",)) p_daemon.daemon = True # 设置为守护进程 p_daemon.start() time.sleep(2) # 主进程等待2秒后结束 print("主进程结束,守护进程被强制销毁") # ========== 案例2:主动终止子进程 ========== print("\n===== 测试terminate() =====") p_terminate = multiprocessing.Process(target=long_task, args=("普通进程",)) p_terminate.start() time.sleep(2) # 让子进程运行2秒 p_terminate.terminate() # 主动终止子进程 p_terminate.join() # 等待子进程彻底终止 print("主进程主动终止子进程,子进程停止运行")案例1:
案例2:
关键说明:
- 守护进程:主进程执行 2 秒后结束,守护进程被直接销毁,不会继续运行;
- terminate():主进程主动调用该方法,强制终止子进程,即使子进程是无限循环也会停止;
- 注意:terminate()后建议调用join(),确保子进程彻底释放资源。
进程同步
若多个进程需要操作共享资源(如文件),需使用multiprocessing.Lock实现锁机制,防止数据竞争;若需严格控制进程执行顺序,可使用multiprocessing.Event实现进程间信号传递(规定执行顺序)。
场景1:Lock锁机制
核心逻辑
多个进程同时写入同一个文件时,会出现 “数据错乱”,通过Lock加锁,保证同一时间只有一个进程写入。
import multiprocessing import time def write_file(num, lock): # 加锁:确保同一时间只有一个进程执行写入操作 lock.acquire() try: with open("test.txt", "a", encoding="utf-8") as f: f.write(f"进程{num}写入:{time.ctime()}\n") print(f"进程{num}写入完成") time.sleep(0.5) # 模拟写入耗时 finally: # 必须释放锁,否则其他进程会一直等待 lock.release() if __name__ == '__main__': # 初始化锁 lock = multiprocessing.Lock() # 清空文件(方便测试) with open("test.txt", "w", encoding="utf-8") as f: f.write("") # 创建5个进程同时写入文件 processes = [] for i in range(5): p = multiprocessing.Process(target=write_file, args=(i, lock)) processes.append(p) p.start() for p in processes: p.join() print("所有进程写入完成,查看test.txt内容:") with open("test.txt", "r", encoding="utf-8") as f: print(f.read())
关键说明
- 不加锁时,多个进程的写入内容会 “混在一起”(比如一行内容被截断);
- 加锁后,写入操作串行执行,数据完整无错乱;
- 建议用try...finally包裹锁操作,确保即使发生异常也能释放锁。
场景2:Event事件
核心逻辑
通过Event的set()(设置信号)和wait()(等待信号),强制让进程 2 必须等进程 1 执行完后再运行。
import multiprocessing import time def process1(event): print("进程1开始执行任务...") time.sleep(2) # 模拟任务耗时 print("进程1任务完成,发送信号让进程2执行") event.set() # 设置信号,通知等待的进程 def process2(event): print("进程2等待进程1完成...") event.wait() # 等待信号,未收到信号则阻塞 print("进程2开始执行任务...") time.sleep(1) print("进程2任务完成") if __name__ == '__main__': # 初始化事件(初始状态为False) event = multiprocessing.Event() p1 = multiprocessing.Process(target=process1, args=(event,)) p2 = multiprocessing.Process(target=process2, args=(event,)) # 启动进程(先启动p2,验证p2会等待p1) p2.start() time.sleep(0.5) # 确保p2先进入等待状态 p1.start() p1.join() p2.join() print("所有进程执行完毕")
关键说明
- 即使先启动 p2,p2 也会卡在event.wait( )处,直到 p1 调用event.set( )发送信号;
- Event适合 “进程 A 必须等进程 B 完成某步操作后再执行” 的场景,比join()更灵活(join( )是等整个进程结束,Event可控制进程内的某一步)。
2.4、进程的优缺点
- 优点:可利用多核 CPU 实现并行执行,多个进程相互独立,一个进程崩溃不会影响其他进程;
- 缺点:创建进程的资源开销大(内存、CPU 资源),进程间通信需要借助管道、消息队列等 IPC 机制,实现复杂。
三、线程:CPU 调度的最小单位
3.1、线程的核心概念
线程(Thread)是程序执行的最小单位,是进程的组成部分,一个进程至少包含一个主线程,也可创建多个子线程。
进程仅负责分配资源,而线程才是真正执行程序的主体。同一进程的所有线程共享进程的内存空间和系统资源(如全局变量、文件句柄),就像一个 QQ 程序(进程)打开多个聊天窗口(线程),无需重复分配资源,因此线程的资源开销远低于进程。
3.2、多线程的实现(threading 模块)
Python 中通过threading模块实现多线程,实现步骤与多进程高度相似,降低了学习成本,同样支持args/kwargs传参。
import threading import time # 定义任务函数 def music(num): for i in range(num): print("听音乐...") time.sleep(0.2) def coding(count): for i in range(count): print("敲代码...") time.sleep(0.2) if __name__ == '__main__': # 创建线程对象 t1 = threading.Thread(target=music, args=(3,)) t2 = threading.Thread(target=coding, kwargs={"count": 3}) # 启动线程 t1.start() t2.start() # 等待子线程执行完毕 t1.join() t2.join() print("程序结束")
3.3、线程的关键知识点
- 线程的执行顺序:线程间的执行是无序的,由 CPU 调度随机决定,可通过threading.current_thread()获取当前线程信息,查看线程执行状态。
- 线程间共享全局变量:同一进程的所有线程共享全局变量,这是线程的优势,但也会带来数据竞争问题(多个线程同时修改全局变量),需通过threading.Lock加锁解决。
- 守护线程:线程无法像进程一样被主动销毁,若需主线程结束时子线程停止,只能设置守护线程:t1.daemon = True。
- GIL 锁的限制:Python 中的全局解释器锁(GIL)导致同一时刻只有一个线程能执行 Python 字节码,因此多线程无法利用多核 CPU 实现并行,仅适用于 I/O 密集型任务(如网络请求、文件读写),而非 CPU 密集型任务。
3.4、线程的优缺点
- 优点:创建线程的资源开销小、启动速度快,线程间共享资源,通信简单;
- 缺点:受 GIL 锁限制,无法利用多核 CPU,线程间共享资源易引发数据竞争,需加锁同步,一个线程崩溃可能导致整个进程崩溃。
更多推荐













所有评论(0)