【自记】Python 线程&进程:多线程的参数&常用方法、守护线程、主子线程,多进程的参数&常用方法、父子进程,以及锁机制,最后通过案例逐一演示
类型定义与角色主线程程序启动时,操作系统(OS)自动创建的默认线程,是程序的 “总控制器”:1. 负责执行程序的入口代码(如块);2. 可手动创建其他子线程;MainThread(可通过查看)。非主线程由主线程或其他子线程通过手动创建的线程,用于 “并行” 执行耗时任务(如 IO 操作),避免阻塞主线程。Thread-1Thread-2...(可自定义)。类型定义与角色父进程启动后创建其他进程的
一、线程基础:threading
模块核心元素
1. threading.Thread
类(创建线程的核心)
用于创建和管理线程,初始化时的参数及作用如下:
参数 | 作用说明 | 默认值 |
---|---|---|
group |
预留参数,用于线程组管理(目前未实现,无需设置) | None |
target |
线程要执行的目标函数(可调用对象,如函数、方法) | None |
name |
线程名称(便于调试识别,不设置则自动生成如 Thread-1 ) |
自动分配 |
args |
传递给 target 函数的位置参数(元组形式,如 (1, 2) ) |
() |
kwargs |
传递给 target 函数的关键字参数(字典形式,如 {'a': 1} ) |
{} |
daemon |
是否为守护线程(True 时,主线程结束则强制终止;False 则需等待完成) |
继承父线程 |
2. 线程常用方法与属性
方法 / 属性 | 作用说明 |
---|---|
start() |
启动线程(让线程进入就绪状态,等待 CPU 调度执行 target 函数) |
join(timeout=None) |
阻塞当前线程(通常是主线程),等待被调用线程执行完毕(timeout 为超时秒数) |
is_alive() |
返回 True 表示线程已启动且未结束(存活状态) |
name / getName() |
获取线程名称(name 是属性,getName() 是方法,效果相同) |
setName(name) |
设置线程名称(也可直接赋值 thread.name = "新名称" ) |
daemon / isDaemon() |
获取是否为守护线程(daemon 是属性,isDaemon() 是方法) |
setDaemon(daemon) |
设置是否为守护线程(也可直接赋值 thread.daemon = True ) |
3. threading.current_thread()
函数
- 作用:返回当前正在执行的线程对象。
- 用途:在多线程中获取当前线程的信息(如名称、是否为守护线程等),常用于日志输出或线程身份判断。
二、案例演示:从基础到进阶
案例 1:基础线程创建(参数 target
、name
、args
与方法 start()
、join()
)
目标:演示如何用基本参数创建线程,以及 start()
(启动)和 join()
(等待)的作用。
import threading
import time
# 定义线程要执行的目标函数(带参数)
def print_info(num, text):
# 用 current_thread() 获取当前线程对象,打印线程名称
print(f"当前线程:{threading.current_thread().name},参数:num={num}, text={text}")
time.sleep(2) # 模拟任务耗时
# 1. 创建线程:指定 target、name、args
# - target:要执行的函数 print_info
# - name:线程名称(便于识别)
# - args:传递给 print_info 的位置参数(元组形式)
thread1 = threading.Thread(
target=print_info,
name="线程A",
args=(1, "Hello") # 对应 print_info 的 num=1, text="Hello"
)
thread2 = threading.Thread(
target=print_info,
name="线程B",
args=(2, "World")
)
# 2. 启动线程:调用 start() 后,线程进入就绪状态,等待 CPU 调度
print("启动线程A和线程B...")
thread1.start()
thread2.start()
# 3. 等待线程完成:主线程调用 join() 后会阻塞,直到线程执行完毕
print("主线程等待线程A和线程B结束...")
thread1.join() # 等待线程A完成
thread2.join() # 等待线程B完成
print("所有线程执行完毕,主线程结束")
执行详解:
thread1.start()
和thread2.start()
启动线程后,CPU 会调度它们交替执行(宏观上并行)。threading.current_thread().name
在print_info
中获取当前执行线程的名称(“线程 A” 或 “线程 B”)。join()
确保主线程等待子线程完成后再结束,避免主线程提前退出导致子线程被中断。
输出结果:
启动线程A和线程B...
主线程等待线程A和线程B结束...
当前线程:线程A,参数:num=1, text=Hello
当前线程:线程B,参数:num=2, text=World
(等待2秒)
所有线程执行完毕,主线程结束
案例 2:守护线程(daemon
属性)与 is_alive()
方法
目标:演示守护线程的特性(随主线程结束而终止)和 is_alive()
方法(判断线程是否存活)。
import threading
import time
# 守护线程任务:模拟后台监控(无限循环)
def background_monitor():
while True:
# 获取当前线程信息(是否为守护线程)
current = threading.current_thread()
print(f"[{current.name}] 后台监控中(守护线程:{current.daemon})")
time.sleep(1) # 每秒打印一次
# 非守护线程任务:模拟主任务(执行3秒后结束)
def main_task():
print(f"[{threading.current_thread().name}] 主任务开始")
time.sleep(3)
print(f"[{threading.current_thread().name}] 主任务结束")
# 1. 创建守护线程
monitor_thread = threading.Thread(
target=background_monitor,
name="监控线程"
)
monitor_thread.daemon = True # 设置为守护线程
# 2. 创建非守护线程
main_thread = threading.Thread(
target=main_task,
name="主任务线程"
)
# 非守护线程默认 daemon=False,无需额外设置
# 3. 启动线程
monitor_thread.start()
main_thread.start()
# 4. 用 is_alive() 监控线程状态(每隔1秒检查一次)
for i in range(4):
print(f"\n第 {i+1} 秒线程状态:")
print(f"监控线程是否存活:{monitor_thread.is_alive()}")
print(f"主任务线程是否存活:{main_thread.is_alive()}")
time.sleep(1)
# 5. 等待非守护线程结束(守护线程无需等待)
main_thread.join()
print("\n主线程:所有非守护线程已结束,准备退出(守护线程将被终止)")
执行详解:
- 守护线程
monitor_thread
因daemon=True
,当所有非守护线程(main_thread
和主线程)结束时,会被强制终止(即使处于无限循环)。 is_alive()
用于判断线程是否存活:前 3 秒内,main_thread
存活;第 4 秒时,main_thread
已结束,is_alive()
返回False
。
输出结果:
[监控线程] 后台监控中(守护线程:True)
[主任务线程] 主任务开始
第 1 秒线程状态:
监控线程是否存活:True
主任务线程是否存活:True
[监控线程] 后台监控中(守护线程:True)
第 2 秒线程状态:
监控线程是否存活:True
主任务线程是否存活:True
[监控线程] 后台监控中(守护线程:True)
第 3 秒线程状态:
监控线程是否存活:True
主任务线程是否存活:True
[主任务线程] 主任务结束
[监控线程] 后台监控中(守护线程:True)
第 4 秒线程状态:
监控线程是否存活:True
主任务线程是否存活:False
主线程:所有非守护线程已结束,准备退出(守护线程将被终止)
案例 3:关键字参数 kwargs
与线程命名方法
目标:演示 kwargs
传递关键字参数,以及 setName()
方法(或 name
属性)设置线程名称。
import threading
import time
# 守护线程任务:模拟后台监控(无限循环)
def background_monitor():
while True:
# 获取当前线程信息(是否为守护线程)
current = threading.current_thread()
print(f"[{current.name}] 后台监控中(守护线程:{current.daemon})")
time.sleep(1) # 每秒打印一次
# 非守护线程任务:模拟主任务(执行3秒后结束)
def main_task():
print(f"[{threading.current_thread().name}] 主任务开始")
time.sleep(3)
print(f"[{threading.current_thread().name}] 主任务结束")
# 1. 创建守护线程
monitor_thread = threading.Thread(
target=background_monitor,
name="监控线程"
)
monitor_thread.daemon = True # 设置为守护线程
# 2. 创建非守护线程
main_thread = threading.Thread(
target=main_task,
name="主任务线程"
)
# 非守护线程默认 daemon=False,无需额外设置
# 3. 启动线程
monitor_thread.start()
main_thread.start()
# 4. 用 is_alive() 监控线程状态(每隔1秒检查一次)
for i in range(4):
print(f"\n第 {i+1} 秒线程状态:")
print(f"监控线程是否存活:{monitor_thread.is_alive()}")
print(f"主任务线程是否存活:{main_thread.is_alive()}")
time.sleep(1)
# 5. 等待非守护线程结束(守护线程无需等待)
main_thread.join()
print("\n主线程:所有非守护线程已结束,准备退出(守护线程将被终止)")
小结
-
线程创建参数:
target
是线程执行的核心函数,args
和kwargs
用于传递参数。name
用于标识线程,daemon
控制线程是否随主线程终止。
-
关键方法:
start()
:唯一启动线程的方法(不可重复调用)。join()
:确保主线程等待子线程完成,避免异步执行导致的逻辑错误。is_alive()
:判断线程状态,常用于监控或条件判断(如 “线程存活时不重复启动”)。
-
threading.current_thread()
:
在函数内部获取当前执行的线程对象,可用于获取线程名称、判断是否为守护线程等,是多线程调试的重要工具。
通过这些知识点,可灵活实现多线程并发任务,尤其适合 I/O 密集型场景(如网络请求、文件读写)。对于 CPU 密集型任务,需结合多进程(multiprocessing
)避开 GIL 限制。
三、主线程与子线程
线程是 进程内的执行单元,一个进程至少包含一个线程(主线程)。主线程是程序的 “入口”,非主线程(常称 “子线程”)是手动创建的并行执行单元,二者共享进程的内存空间(如全局变量、堆内存)。
1. 核心定义
类型 | 定义与角色 |
---|---|
主线程 | 程序启动时,操作系统(OS)自动创建的默认线程,是程序的 “总控制器”: 1. 负责执行程序的入口代码(如 if __name__ == "__main__": 块);2. 可手动创建其他子线程; 3. 默认名: MainThread (可通过 thread.name 查看)。 |
非主线程 | 由主线程或其他子线程通过 threading.Thread 手动创建的线程,用于 “并行” 执行耗时任务(如 IO 操作),避免阻塞主线程。默认名: Thread-1 、Thread-2 ...(可自定义)。 |
2. 关键特点
- 资源共享:同一进程内的所有线程共享进程的内存空间(全局变量、函数、类实例等),无需额外通信机制即可访问(但需注意线程安全,如用
threading.Lock
避免竞争)。 - 执行调度:线程由 OS 调度器统一管理,执行顺序是 “抢占式” 的(无法预测哪个线程先执行),取决于 OS 的调度策略。
- 生命周期关联:
- 非守护线程(默认):主线程会等待所有非守护线程执行完毕后才结束(即使主线程代码已执行完);
- 守护线程(
daemon=True
):主线程结束时,守护线程会被强制终止(无论是否执行完),常用于 “辅助任务”(如日志打印)。
3. 核心方法与属性(必掌握)
方法 / 属性 | 作用说明 |
---|---|
threading.current_thread() |
获取当前正在执行的线程对象,常用于区分主线程与子线程。 |
threading.Thread(target, args, kwargs, name, daemon) |
创建子线程的核心类: - target :线程要执行的函数(必填);- args :传给 target 的位置参数(元组,空元组需写 () );- kwargs :传给 target 的关键字参数(字典);- name :自定义线程名(字符串,可选);- daemon :是否设为守护线程(布尔值,可选,默认 False )。 |
thread.start() |
启动线程:OS 会调用线程的 run() 方法(不可直接调用 run() ,否则会变成主线程内的普通函数执行)。 |
thread.join(timeout=None) |
主线程阻塞等待该子线程执行完毕(可选 timeout :超时时间,单位秒);若不调用 join() ,主线程可能先于子线程结束。 |
thread.name |
获取 / 设置线程名(如 current_thread().name 查看当前线程名)。 |
thread.is_alive() |
判断线程是否仍在执行(返回布尔值)。 |
4. 代码案例(带详细注释)
案例 1:区分主线程与子线程,观察执行顺序
import threading
import time
# 定义子线程要执行的函数
def task(task_name, sleep_time):
# 获取当前执行该函数的线程对象
current_thread = threading.current_thread()
print(f"【{current_thread.name}】开始执行任务:{task_name}")
time.sleep(sleep_time) # 模拟耗时操作(IO/计算)
print(f"【{current_thread.name}】完成任务:{task_name}")
if __name__ == "__main__":
# 1. 查看主线程信息
main_thread = threading.current_thread()
print(f"=== 主线程启动:{main_thread.name},线程ID:{main_thread.ident} ===")
# 2. 创建2个子线程(非守护线程,默认)
# 子线程1:用 args 传位置参数(元组,注意逗号)
thread1 = threading.Thread(
target=task,
args=("下载文件", 2), # 对应 task 的 (task_name, sleep_time)
name="FileDownloadThread" # 自定义线程名
)
# 子线程2:用 kwargs 传关键字参数(字典)
thread2 = threading.Thread(
target=task,
kwargs={"task_name": "处理数据", "sleep_time": 1},
name="DataProcessThread"
)
# 3. 启动子线程(OS 开始调度)
thread1.start()
thread2.start()
# 4. 主线程等待子线程执行完毕(若注释 join(),主线程会先打印“主线程结束”)
thread1.join() # 主线程阻塞,直到 thread1 完成
thread2.join() # 主线程阻塞,直到 thread2 完成
# 5. 主线程执行收尾工作
print(f"=== 主线程 {main_thread.name} 结束 ===")
执行结果(顺序可能因 OS 调度变化):
=== 主线程启动:MainThread,线程ID:140703324872512 ===
【FileDownloadThread】开始执行任务:下载文件
【DataProcessThread】开始执行任务:处理数据
【DataProcessThread】完成任务:处理数据 # 1秒后完成
【FileDownloadThread】完成任务:下载文件 # 2秒后完成
=== 主线程 MainThread 结束 ===
关键说明:
- 通过
threading.current_thread()
明确了 “哪个线程在执行任务”; join()
确保主线程等待子线程完成后再结束,若删除join()
,主线程会直接打印 “结束”,但子线程仍会继续执行(因是非守护线程)。
四、父进程与子进程
进程是 OS 分配资源的基本单位(如内存、CPU),一个程序运行后至少对应一个进程(父进程)。父进程通过 multiprocessing
等模块创建子进程,二者拥有完全独立的内存空间(资源不共享)。
1. 核心定义
类型 | 定义与角色 |
---|---|
父进程 | 启动后创建其他进程的 “初始进程”: 1. 如运行 python script.py 时,OS 创建的进程就是父进程;2. 父进程可通过 multiprocessing.Process 创建子进程;3. 每个进程有唯一的 PID(进程 ID),父进程的 PID 称为 PPID(父进程 ID)。 |
子进程 | 由父进程通过 OS 系统调用(如 Unix 的 fork() 、Windows 的 CreateProcess() )创建的新进程:1. 子进程会复制父进程的代码段、数据段(但后续修改不影响父进程,即 “写时复制”); 2. 子进程有独立的内存空间,全局变量、栈内存与父进程完全隔离。 |
2. 关键特点
- 资源隔离:父子进程的内存空间完全独立(全局变量、函数参数等不共享),子进程修改全局变量不会影响父进程(与线程的核心区别)。
- 创建开销大:进程创建需要 OS 分配新的内存、CPU 资源,开销远大于线程(线程共享进程资源,创建快)。
- 生命周期关联:
- 子进程默认不随父进程结束而结束(父进程结束后,子进程会成为 “孤儿进程”,由 OS 的 “init 进程” 或 “systemd” 收养);
- 父进程若未回收子进程资源(如 exit code),子进程会成为 “僵尸进程”(占用 PID 等资源)。
- 通信需 IPC 机制:父子进程需通过 跨进程通信(IPC) 交换数据,如
Queue
(队列)、Pipe
(管道)、共享内存、Socket 等(multiprocessing
封装了这些机制)。
3. 核心方法与属性(必掌握)
方法 / 属性 | 作用说明 |
---|---|
multiprocessing.Process(target, args, kwargs, name) |
创建子进程的核心类(用法类似 threading.Thread ):- target :子进程要执行的函数;- args /kwargs :传给函数的参数(同线程);- name :自定义进程名。 |
process.start() |
启动子进程:OS 会创建新进程并执行 target 函数(不可直接调用 run() )。 |
process.join(timeout=None) |
父进程阻塞等待子进程执行完毕(避免子进程成为僵尸进程)。 |
process.pid |
获取子进程的 PID(进程 ID,唯一标识)。 |
os.getppid() |
在子进程中调用,获取父进程的 PID(需导入 os 模块)。 |
multiprocessing.Queue() |
跨进程安全的队列,用于父子进程通信(put() 存数据,get() 取数据)。 |
process.terminate() |
强制终止子进程(类似 OS 的 kill 命令,需谨慎使用)。 |
4. 代码案例(带详细注释)
案例 1:区分父进程与子进程,查看 PID/PPID
import multiprocessing
import os
import time
# 定义子进程要执行的函数
def child_task(task_name):
# 子进程中获取自身 PID 和父进程 PPID
child_pid = os.getpid()
parent_pid = os.getppid()
print(f"【子进程】名称:{multiprocessing.current_process().name}")
print(f"【子进程】PID:{child_pid},父进程 PPID:{parent_pid}")
print(f"【子进程】执行任务:{task_name},耗时2秒...")
time.sleep(2)
print(f"【子进程】任务 {task_name} 完成!")
if __name__ == "__main__":
# 1. 父进程信息
parent_process = multiprocessing.current_process()
parent_pid = os.getpid()
print(f"=== 父进程启动 ===")
print(f"【父进程】名称:{parent_process.name},PID:{parent_pid}")
# 2. 创建子进程(注意:Windows 下必须放在 if __name__ == "__main__": 内,避免递归创建)
child_process = multiprocessing.Process(
target=child_task,
args=("处理大数据",), # 位置参数(元组)
name="BigDataProcess" # 自定义子进程名
)
# 3. 启动子进程
child_process.start()
# 4. 父进程等待子进程完成(避免子进程成为僵尸进程)
child_process.join()
print(f"=== 父进程 PID:{parent_pid} 结束 ===")
执行结果:
=== 父进程启动 ===
【父进程】名称:MainProcess,PID:1234
【子进程】名称:BigDataProcess
【子进程】PID:5678,父进程 PPID:1234
【子进程】执行任务:处理大数据,耗时2秒...
【子进程】任务 处理大数据 完成!
=== 父进程 PID:1234 结束 ===
关键说明:
- 子进程的
PPID
等于父进程的PID
,证明父子关系; - Windows 下必须将
Process
创建放在if __name__ == "__main__":
内:因为 Windows 没有fork()
,会通过 “导入模块” 创建子进程,若不限制会递归创建进程。
案例 2:验证进程资源隔离 + 用 Queue 实现通信
import multiprocessing
import time
# 全局变量(验证隔离性)
global_var = "父进程初始值"
def child_task(queue):
"""子进程任务:修改全局变量 + 向队列传数据"""
global global_var # 声明使用全局变量
print(f"【子进程】初始 global_var:{global_var}") # 复制父进程的初始值
global_var = "子进程修改后的值" # 修改全局变量
print(f"【子进程】修改后 global_var:{global_var}")
# 向队列传递数据(子进程 → 父进程)
queue.put(f"子进程传递的数据:当前时间 {time.ctime()}")
time.sleep(1)
if __name__ == "__main__":
# 1. 创建跨进程队列(用于父子通信)
comm_queue = multiprocessing.Queue()
# 2. 创建子进程
child_process = multiprocessing.Process(
target=child_task,
args=(comm_queue,) # 队列作为参数传递(需可序列化)
)
# 3. 启动子进程
child_process.start()
child_process.join()
# 4. 父进程查看全局变量(验证隔离性)
print(f"\n【父进程】全局变量 global_var:{global_var}") # 仍为初始值,未被修改
# 5. 父进程从队列获取子进程数据(通信)
received_data = comm_queue.get() # 阻塞等待队列数据
print(f"【父进程】收到子进程数据:{received_data}")
执行结果:
【子进程】初始 global_var:父进程初始值
【子进程】修改后 global_var:子进程修改后的值
【父进程】全局变量 global_var:父进程初始值 # 资源隔离,子进程修改不影响父进程
【父进程】收到子进程数据:子进程传递的数据:当前时间 Wed Sep 3 10:00:00 2025
关键说明:
- 资源隔离:子进程修改的
global_var
是其独立内存中的副本,父进程的global_var
完全不受影响(与线程共享全局变量形成鲜明对比); - 跨进程通信:
multiprocessing.Queue
是线程安全、进程安全的,通过序列化(pickle)实现数据传递,支持父子进程双向通信。
小结
- 主线程是 “入口”,子线程是 “并行单元”:共享内存,适合 IO 密集型任务,需注意线程安全;
- 父进程是 “创建者”,子进程是 “独立个体”:内存隔离,适合 CPU 密集型任务,需通过 IPC 通信;
- 核心选择依据:IO 密集用线程(高效),CPU 密集用进程(利用多核),复杂场景可结合(如多进程 + 多线程)。
五、锁机制
在多线程 / 多进程编程中,当多个执行单元(线程 / 进程)同时操作共享资源(如全局变量、文件、数据库)时,可能会出现竞争条件(Race Condition)—— 即资源状态因并发操作而变得不一致。锁机制是解决这一问题的核心手段,它通过 “互斥访问” 确保同一时间只有一个执行单元能操作共享资源。
线程锁(threading.Lock
):解决线程间资源竞争
线程共享进程的内存空间,全局变量、函数参数等是典型的共享资源。线程锁用于保证同一时间只有一个线程能执行 “临界区代码”(操作共享资源的代码段)。
1. 线程锁的核心方法
方法 | 作用说明 |
---|---|
lock = threading.Lock() |
创建一个线程锁对象(初始为 “未锁定” 状态)。 |
lock.acquire(blocking=True, timeout=-1) |
尝试获取锁: - 若锁未被占用,当前线程获取锁(锁变为 “锁定” 状态),继续执行; - 若锁已被占用, blocking=True 时阻塞等待(默认),timeout 设超时时间(秒);- 成功获取返回 True ,超时返回 False 。 |
lock.release() |
释放锁(锁变为 “未锁定” 状态),必须由持有锁的线程调用,否则报错。 |
with lock: |
上下文管理器语法(推荐):自动获取锁,代码块执行完后自动释放(即使报错也会释放,避免死锁)。 |
2. 案例:无锁导致的混乱 vs 有锁的正确同步
场景:3 个线程同时给一个全局变量(余额)加 1,各加 1000 次,预期结果是 3000。
import threading
import time
# 共享资源:全局变量(余额)
balance = 0
def add_money(lock=None):
global balance
for _ in range(1000):
# 临界区:操作共享资源的代码
if lock:
# 方案2:有锁(用with自动获取和释放锁)
with lock:
current = balance
time.sleep(0.0001) # 模拟耗时操作(放大竞争问题)
balance = current + 1
else:
# 方案1:无锁(会出现竞争条件)
current = balance
time.sleep(0.0001) # 模拟耗时操作(放大竞争问题)
balance = current + 1
# 测试无锁情况
balance = 0
threads = []
for _ in range(3):
t = threading.Thread(target=add_money)
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"无锁时的结果:{balance}(预期3000,实际可能小于3000)")
# 测试有锁情况
balance = 0
lock = threading.Lock() # 创建线程锁
threads = []
for _ in range(3):
# 将锁传给线程函数
t = threading.Thread(target=add_money, args=(lock,))
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"有锁时的结果:{balance}(预期3000,实际正确)")
执行结果分析:
- 无锁情况:多个线程同时读取
balance
,修改后写回,导致 “覆盖”(如线程 1 和线程 2 同时读balance=100
,都加 1 后写回 101,实际应是 102),最终结果小于 3000。 - 有锁情况:
with lock
确保同一时间只有一个线程执行current = balance
到balance = current + 1
的逻辑,避免覆盖,结果正确为 3000。
进程锁(multiprocessing.Lock
):解决进程间资源竞争
进程内存隔离,但可能共享外部资源(如文件、数据库、打印机)。进程锁用于保证同一时间只有一个进程能操作这些共享资源。
1. 进程锁的核心方法
与线程锁完全一致(接口设计统一):
lock = multiprocessing.Lock()
创建进程锁;acquire()
/release()
或with lock:
控制资源访问。
2. 案例:多进程写入同一文件的同步问题
场景:3 个进程同时向一个文件写入内容,无锁会导致内容混乱,有锁则保证顺序写入。
import multiprocessing
import time
def write_file(process_id, lock):
# 整个循环放入with块:持有锁直到写完所有3行
with lock:
for i in range(3):
content = f"进程{process_id}的第{i+1}行内容\n"
with open("sequential_file.txt", "a", encoding="utf-8") as f:
f.write(content)
time.sleep(0.1) # 模拟耗时,此时仍持有锁
# 清空文件
with open("sequential_file.txt", "w", encoding="utf-8") as f:
pass
lock = multiprocessing.Lock()
processes = [
multiprocessing.Process(target=write_file, args=(0, lock)),
multiprocessing.Process(target=write_file, args=(1, lock)),
multiprocessing.Process(target=write_file, args=(2, lock))
]
for p in processes:
p.start()
for p in processes:
p.join()
print("写入完成,文件内容按进程顺序排列")
输出结果(严格按进程顺序):
进程0的第1行内容
进程0的第2行内容
进程0的第3行内容
进程1的第1行内容
进程1的第2行内容
进程1的第3行内容
进程2的第1行内容
进程2的第2行内容
进程2的第3行内容
原因:进程 0 获取锁后,会持有锁直到写完 3 行才释放;之后进程 1 可能获取锁写 3 行,最后进程 2 写 3 行。但这种方式并发效率低(相当于串行执行),只适合必须严格顺序的场景。
死锁问题与避免
死锁是锁机制的常见陷阱:当多个执行单元(线程 / 进程)互相等待对方释放锁时,程序会陷入无限阻塞。
1. 死锁示例(多线程场景)
import threading
# 创建两个锁
lock1 = threading.Lock()
lock2 = threading.Lock()
def task1():
with lock1: # 获取lock1
print("任务1获取了lock1,等待lock2...")
with lock2: # 尝试获取lock2,但此时可能被task2持有
print("任务1获取了lock2,执行完成")
def task2():
with lock2: # 获取lock2
print("任务2获取了lock2,等待lock1...")
with lock1: # 尝试获取lock1,但此时可能被task1持有
print("任务2获取了lock1,执行完成")
t1 = threading.Thread(target=task1)
t2 = threading.Thread(target=task2)
t1.start()
t2.start()
t1.join()
t2.join()
执行结果:程序会卡住,输出:
任务1获取了lock1,等待lock2...
任务2获取了lock2,等待lock1...
2. 死锁避免策略
- 按顺序获取锁:所有执行单元严格按同一顺序获取多个锁(如先获取
lock1
再获取lock2
)。 - 设置超时时间:用
lock.acquire(timeout=5)
避免无限等待,超时后释放已获取的锁。 - 使用可重入锁(
RLock
):同一线程可多次获取同一把锁(threading.RLock
),避免自己阻塞自己。
更多推荐
所有评论(0)