线程

线程的概念和作用

线程(Thread)是操作系统中进程内的执行单元,是调度和执行的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源(如内存、文件句柄等),但每个线程有独立的执行路径(如栈、寄存器状态等)。

一、线程的核心概念

  1. 线程的定义
    • 线程是进程的子任务,是 CPU 调度的最小单位。
    • 一个进程至少包含一个线程(主线程),也可以创建多个线程。
  2. 线程与进程的关系
    • 进程是资源分配的单位,拥有独立的内存空间和系统资源。
    • 线程是执行任务的单位,共享进程的资源,但独立运行。
  3. 线程的特性
    • 轻量级:线程的创建、切换和销毁开销远小于进程。
    • 资源共享:线程共享进程的内存、文件描述符等资源,通信高效。
    • 并发性:多个线程可以并行执行(取决于 CPU 核数和系统支持)。

二、线程的核心作用

1. 提高程序效率(并发执行)

  • I/O 密集型任务:当线程等待 I/O 操作(如读写文件、网络请求)时,其他线程可以继续执行,提升整体效率。
  • 示例:在 Web 服务器中,每个客户端请求由一个线程处理,避免阻塞其他请求。

2. 实现多任务协作

  • 多个线程可以协作完成复杂任务,例如:
    • 一个线程负责数据生成,另一个线程负责数据处理。
    • GUI 应用中,主线程负责界面更新,子线程负责后台计算。

3. 资源共享与通信

  • 线程共享进程的内存和资源,可以直接访问全局变量和共享数据,通信更简单高效。
  • 示例:多个线程共同修改一个共享变量(需同步机制避免冲突)。

4. 简化程序设计

  • 在单进程中使用多线程,避免进程间通信(IPC)的复杂性。
  • 适合需要频繁交互的任务(如多线程爬虫、实时数据处理)。

多线程的使用

import threading
import time
def task1():
    for i in range(5):
        print(f"任务1 - 计数: {i}")
        time.sleep(1)
def task2():
    for i in range(5):
        print(f"任务2 - 计数: {i}")
        time.sleep(1)
def main():
    # 创建线程
    thread1 = threading.Thread(target=task1)
    thread2 = threading.Thread(target=task2)
    # 启动线程
    thread1.start()
    thread2.start()
    # 等待线程完成
    thread1.join()
    thread2.join()
    print("所有任务完成。")
if __name__ == "__main__":
    main()
线程

获取线程对象和线程名

获取对象:如果你需要在一个运行的线程内获取其对应的线程对象本身,Python 提供了一个方便的方法:threading.current_thread()。这个函数返回当前执行线程的 Thread 对象。你可以在任何线程内调用这个函数来获取当前线程的对象,并访问它的各种属性。

获取线程名:可以通过Thread对象的 name 属性来获取和设置线程名。此外,线程对象本身可以通过变量来进行访问

import threading
import time

def task():
    # 获取当前线程对象
    current_thread = threading.current_thread()
    # 输出线程名和线程对象
    print(f"当前线程对象: {current_thread}, 线程名: {current_thread.name}")
    for i in range(5):
        print(f"{current_thread.name} - 计数: {i}")
        time.sleep(1)

def main():
    # 创建线程并设置名称
    thread1 = threading.Thread(target=task, name="线程1")
    thread2 = threading.Thread(target=task, name="线程2")

    # 启动线程
    thread1.start()
    thread2.start()

    # 打印线程对象信息
    print(f"主线程中的线程1对象: {thread1}")
    print(f"主线程中的线程2对象: {thread2}")
    
    # 等待线程完成
    thread1.join()
    thread2.join()

    print("所有任务完成。")

if __name__ == "__main__":
    main()
线程

解释:

  • 获取当前线程: 在 task 函数中,使用 threading.current_thread() 来获取当前线程对象,并输出它的名称和对象本身。这会在每个线程启动后立即显示。
  • 在主线程中显示线程对象: 在 main 函数中,创建了 thread1 和 thread2,并在启动它们之后也打印了它们的对象信息。这显示了在主线程中访问线程对象时的信息。

线程执行带有参数的任务

同时使用 args 和 kwargs 传递参数的方式

import threading
import time

def task(name, count, delay=1):
    # 获取当前线程对象
    current_thread = threading.current_thread()
    # 输出线程名和线程对象
    print(f"当前线程对象: {current_thread}, 线程名: {current_thread.name}")
    for i in range(count):
        print(f"{current_thread.name} - {name} - 计数: {i}")
        time.sleep(delay)  # 使用 delay 来控制睡眠时间

def main():
    # 创建线程并设置名称和参数(使用 args)
    thread1 = threading.Thread(target=task, args=("任务1", 5), name="线程1")
    # 创建线程并设置名称和参数(使用 kwargs)
    thread2 = threading.Thread(target=task, kwargs={"name": "任务2", "count": 3, "delay": 2}, name="线程2")

    # 启动线程
    thread1.start()
    thread2.start()

    # 打印线程对象信息
    print(f"主线程中的线程1对象: {thread1}")
    print(f"主线程中的线程2对象: {thread2}")
    
    # 等待线程完成
    thread1.join()
    thread2.join()

    print("所有任务完成。")

if __name__ == "__main__":
    main()

解释:

  1. 任务函数task 接受三个参数:namecount 和 delay。其中 delay 是一个带默认值的参数。
  2. args:
    • args=("任务1", 5): 用于 thread1,传输顺序参数 name 和 count。使用默认的 delay 值。
  3. kwargs:
    • kwargs={"name": "任务2", "count": 3, "delay": 2}: 用于 thread2,传输命名参数 namecount 和 delay。这样可以覆盖默认的 delay 值。

线程的注意点

线程在执行时是无序的

我们创建多个线程,每个线程都会每隔一小段时间打印一条消息。这展示了线程执行时的不确定性和它们交替执行的效果:

import threading
import time

def task(identifier):
    for _ in range(3):
        time.sleep(0.1)  # 每个任务都短暂休眠
        print(f"线程 {identifier} 正在执行")

def main():
    threads = []
    for i in range(5):  # 创建5个线程
        thread = threading.Thread(target=task, args=(i,))
        threads.append(thread)

    for thread in threads:
        thread.start()  # 启动每个线程

    for thread in threads:
        thread.join()  # 等待所有线程完成

if __name__ == "__main__":
    main()
线程

解释:

  1. 随机休眠: 在 task 内使用 random.uniform(0.1, 0.5) 来随机地设置睡眠时间,这有助于模拟线程调度时的非确定性,增强观察效果。
  2. 多线程启动: 从 main 函数中启动多个线程,每个线程输出其名称和计数。这些输出会展示每个线程在执行时的交错。
  3. 无序执行: 由于线程几乎同时启动并随机休眠,你可以观察到每个线程几乎以无序方式输出说明当前正在哪个计数上迭代。

此代码例子展示了线程调度可能在不同时间片的执行顺序,并没有保证按特定顺序运行。

如果有一个线程执行报错,其他线程依然会执行

在多线程编程中,假设一个线程执行过程中遇到错误并引发异常,这并不会阻止其他线程的执行。每个线程是独立的,除非你的代码有机制让线程之间相互影响。

import threading
import time

def task(name, count, delay=1):
    try:
        # 获取当前线程对象
        current_thread = threading.current_thread()
        # 输出线程名和线程对象
        print(f"当前线程对象: {current_thread}, 线程名: {current_thread.name}")
        for i in range(count):
            if name == "任务1" and i == 2:
                raise ValueError("任务1遇到了一个问题")  # 模拟错误
            print(f"{current_thread.name} - {name} - 计数: {i}")
            time.sleep(delay)
    except Exception as e:
        print(f"{current_thread.name} - {name} 报错: {e}")

def main():
    # 创建线程并设置名称和参数
    thread1 = threading.Thread(target=task, args=("任务1", 5), name="线程1")
    thread2 = threading.Thread(target=task, args=("任务2", 5), name="线程2")

    # 启动线程
    thread1.start()
    thread2.start()

    # 打印线程对象信息
    print(f"主线程中的线程1对象: {thread1}")
    print(f"主线程中的线程2对象: {thread2}")
    
    # 等待线程完成
    thread1.join()
    thread2.join()

    print("所有任务完成。")

if __name__ == "__main__":
    main()
线程

解释:

  1. 异常处理: 在 task 函数中,使用 try...except 块来捕获可能出现的异常。在正常执行的过程中,任务1 会在计数到2时故意触发一个 ValueError。捕获此异常后,输出错误信息,但不会停止 任务2 的执行。
  2. 独立执行: 每个线程独立执行它们的代码。即使 任务1 出现异常,任务2 会继续正常运行。

线程执行过程

以这段代码为例:

import threading
import time

def task():
    for i in range(5):
        print(f"[{threading.current_thread().name}] 执行任务 - 计数: {i}")
        time.sleep(1)

def main():
    thread1 = threading.Thread(target=task, name="线程1")
    thread2 = threading.Thread(target=task, name="线程2")

    thread1.start()
    thread2.start()

    thread1.join()
    thread2.join()

    print("[主线程] 所有线程任务完成。")

if __name__ == "__main__":
    main()
    print("[主进程] 程序结束。")
线程

当你运行 Python 脚本时,解释器会自动创建一个「主进程」,也叫 父进程

这个主进程:

  • 会从 if __name__ == "__main__" 这一行开始执行;
  • 然后调用 main() 函数;
  • 整个 main() 函数是在这个主进程内运行的。

你可以这样理解:

# 创建一个主进程(系统帮你创建的)
└── 主进程(PID 12345)
└── 执行 main() 函数
└── 在 main() 内启动线程 thread1、thread2

举个例子确认:你可以加上 os.getpid() 来看进程号(PID):

import os
import threading
import time

def task():
    print(f"[{threading.current_thread().name}] 执行任务中,PID={os.getpid()}")
    time.sleep(1)

def main():
    print(f"[main()] 当前进程PID = {os.getpid()}")  # 主进程
    thread1 = threading.Thread(target=task, name="线程1")
    thread2 = threading.Thread(target=task, name="线程2")

    thread1.start()
    thread2.start()
    thread1.join()
    thread2.join()

    print("[主线程] 所有线程完成")

if __name__ == "__main__":
    main()
    print("[主进程] 程序退出")
线程

注意 PID 是一样的

守护线程

普通线程(非守护线程)

  • 主线程退出时,Python 解释器会等待所有非守护线程结束后才真正退出程序。
  • 因此如果有一个非守护线程还在执行,程序就会“挂着”等它完成。

举个例子:

import threading
import time

def task():
    for i in range(5):
        print(f"普通线程运行中 {i}")
        time.sleep(1)

t = threading.Thread(target=task)  # 默认是非守护线程
t.start()

print("主线程马上结束")
线程

这时程序不会立即退出,会等任务跑完5秒才结束。

守护线程(daemon=True)

  • 适合做“后台服务”,不影响程序生命周期。
  • 守护线程的存在不影响主线程退出。
  • 主线程一退出,守护线程会被立即强制终止(无论是否执行完)。
import threading
import time

def task():
    for i in range(5):
        print(f"普通线程运行中 {i}")
        time.sleep(1)

t = threading.Thread(target=task, daemon=True)
t.start()

print("主线程马上结束")
线程

程序直接退出了,守护线程不会完整跑完。

线程间可以共享全局变量

一个例子,演示线程共享全局变量

这种写法虽然在简单任务里通常没问题,但在复杂并发场景下可能出现竞态条件和数据错乱(等会儿会说)

import threading
import time

counter = 0

def increment():
    global counter
    for _ in range(5):
        counter += 1  # 这里没有加锁
        print(f"[{threading.current_thread().name}] 计数器值: {counter}")
        time.sleep(0.1)

def main():
    thread1 = threading.Thread(target=increment, name="线程1")
    thread2 = threading.Thread(target=increment, name="线程2")

    thread1.start()
    thread2.start()

    thread1.join()
    thread2.join()

    print(f"最终计数器值: {counter}")

if __name__ == "__main__":
    main()
线程

说明(看不懂稍后再看)

  • 两个线程共享变量 counter,都执行 counter += 1
  • 这段代码不使用锁,counter += 1 在 CPython 里是一个相对原子的操作(虽然严格说不是完全线程安全,但在大部分简单环境下表现正常)。
  • 运行多次,通常你会得到正确的结果 最终计数器值: 10,但偶尔在高负载下可能出现小概率错误。

重点提醒

  • Python 的整数对象是不可变的,counter += 1 实际是读取、加一、再赋值的操作,不是完全原子。
  • 这里示范“没有锁”也能共享全局变量,是基于 CPython 的 GIL(全局解释器锁)机制,避免了真正的并发执行。
  • 如果换成多进程或其它语言环境,不加锁则绝对会错。

线程锁

下面给出一段新代码:演示两个线程共享和修改同一个全局变量的例子:

import threading
import time

# 全局变量,所有线程共享
counter = 0

# 线程要执行的函数
def increment():
    global counter
    for _ in range(5):
        temp = counter
        time.sleep(0.1)  # 模拟一些操作延迟
        counter = temp + 1
        print(f"[{threading.current_thread().name}] 计数器值: {counter}")

def main():
    thread1 = threading.Thread(target=increment, name="线程1")
    thread2 = threading.Thread(target=increment, name="线程2")

    thread1.start()
    thread2.start()

    thread1.join()
    thread2.join()

    print(f"最终计数器值: {counter}")

if __name__ == "__main__":
    main()
线程

因为没有加锁,可能会出现“竞态条件”,导致最终结果不是预期的10(2个线程 × 5次递增)。

这里在展示另一个优化后的案例:

import threading
import time

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(5):
        with lock:  # 加锁,保证这一段代码一次只有一个线程执行
            temp = counter
            time.sleep(0.1)  # 模拟操作延迟
            counter = temp + 1
            print(f"[{threading.current_thread().name}] 计数器值: {counter}")

def main():
    thread1 = threading.Thread(target=increment, name="线程1")
    thread2 = threading.Thread(target=increment, name="线程2")

    thread1.start()
    thread2.start()

    thread1.join()
    thread2.join()

    print(f"最终计数器值: {counter}")

if __name__ == "__main__":
    main()
线程

为什么结果不一样?

这就涉及到 Python 线程中「竞态条件」和「操作的原子性」。

两段代码区别分析:

上面这段(不加锁)

temp = counter
time.sleep(0.1)
counter = temp + 1

这个操作过程本质是:

  1. 假设 counter 初始值为 0:
  2. 线程1 执行 temp = counter,此时 temp=0
  3. 线程1 执行 time.sleep(0.1) 挂起
  4. 线程2 执行 temp = counter,此时 temp=0
  5. 线程2 counter = temp + 1,即 counter=1
  6. 线程1 醒来,执行 counter = temp + 1,此时 temp 还是0,counter 又被赋为1,覆盖了线程2的结果

举个实际场景:

counter = 0

线程1读到 temp = 0,然后 sleep

线程2也读到 temp = 0,也 sleep

两个线程醒来后都写入了 counter = 1

最终只加了 1 次,尽管两线程都执行了 +1

这就叫竞态条件(Race Condition),结果不确定,常常小于期望值。

下面这段(加锁版本)

with lock:
    temp = counter
    time.sleep(0.1)
    counter = temp + 1

with lock: 相当于说:这段代码块一次只能有一个线程进入,另一个线程必须等前一个线程释放锁后才能执行。

所以即使 sleep(0.1) 存在,也不会有两个线程在同时读取、同时写入 counter

每个线程对 counter 的操作是原子性的、不会被打断的,所以最终的值总是精确加了 10 次(2线程 × 5次)。

总结:

  • 不加锁版本的关键问题在于:多个线程「同时读-修改-写」共享变量,可能会覆盖彼此的修改,造成数据丢失。
  • 加锁版本通过 threading.Lock,实现了「每次只能有一个线程改数据」,避免了数据错乱,是多线程共享资源的标准做法。

线程之间共享全局变量数据错误问题

下面我们可以带着上面的初步思考,深入了解下竞态条件

什么是竞态条件

当多个线程“同时”访问和修改同一个共享资源(例如全局变量),并且这些访问操作没有适当同步时,就会发生竞态条件。

它的典型表现是:程序执行的结果依赖于线程的执行顺序或时机,因此可能在不同运行中表现出不同的错误结果。

举个例子(数据丢失):

import threading

# 全局变量
counter = 0

# 线程执行的函数
def increment():
    global counter
    for _ in range(100000):
        counter += 1  # 这不是原子操作,会产生竞态条件

def main():
    global counter
    # 创建两个线程
    t1 = threading.Thread(target=increment)
    t2 = threading.Thread(target=increment)

    # 启动线程
    t1.start()
    t2.start()

    # 等待线程结束
    t1.join()
    t2.join()

    print(f"最终 counter 值为: {counter}(理论值应为 200000)")

if __name__ == "__main__":
    main()
线程

如果两个线程并发运行这个函数,理论上 counter 应该最终是 200000,但实际上常常会低于这个值,且每次运行结果可能不同。原因是:

counter += 1 其实包含三个步骤:

  • 读取 counter
  • 加 1
  • 写回 counter

多个线程同时进行这三步时,会互相覆盖对 counter 的修改,导致数据丢失。

为什么会存在竞态条件?

这个问题非常关键,也是多线程编程的“坑王”之一。我们来从 Python 的底层执行机制 详细解析一下:

问题本质

代码中:counter += 1

看起来是一个简单的递增,但底层实际上是三个步骤的复合操作

  1. 读取变量值到寄存器(load)
  2. 对寄存器中的值 +1(add)
  3. 把寄存器的值写回内存(store)

➤ 这不是一个“原子操作”,多个线程可能在不同时间点并发执行这三步,导致数据被覆盖

举个竞态的例子(race condition)

counter = 5,然后:

时间点线程A执行线程B执行counter
t1load counter → 55
t2load counter → 55
t3add 1 → 6add 1 → 6
t4store → counter=6store → counter=6❌ 丢失了一次加1

最终 counter = 6,但本应是 7。

问题出现在哪里?

  • 两个线程都看到原始值为 5,但它们都认为自己是唯一在修改的线程。
  • 最终覆盖对方写入的结果。

为什么 Python 会这样?解释器、GIL 与线程切换

GIL(全局解释器锁)是什么?

Python(CPython)确实有一个 GIL(Global Interpreter Lock),它一次只允许一个线程执行 Python 字节码,所以很多人误以为它能避免并发问题。

误区:GIL 并不能避免数据竞争!

因为:

  • GIL 会在一些“耗时操作”或每执行一定数量的字节码后 自动让出控制权
  • 比如在 time.sleep()、I/O、或操作系统切换上下文时
  • 所以两个线程仍然可以在 counter += 1 还没完成 的中间被打断和切换

总结:因为 counter += 1 不是原子操作,可能被线程打断,多个线程读到旧值并写入,发生覆盖

三种“自动让出控制权”的典型情况

time.sleep():主动让出 CPU

本质:让当前线程“暂停”一段时间(让操作系统把 CPU 时间给其他线程)。

特点:

  • 是显式告诉解释器:“我不用 CPU 了,其他线程可以运行”。
  • Python 会自动释放 GIL,其他线程就可以继续执行。

② I/O 操作:比如读写文件、请求网络、数据库查询

with open("file.txt") as f:
    data = f.read()

requests.get("https://example.com")
  • 本质:线程在等待外部设备或网络返回数据,不用 CPU。
  • Python 解释器会检测到是阻塞 I/O,自动释放 GIL,让其他线程来用 CPU。
  • 实际的读取是 OS 或驱动完成的,线程只是在那“等”。

重点:I/O 是“慢”的操作,不让出 CPU 会浪费资源。

③ 操作系统层级的线程切换

即使你没显式 sleep、没做 I/O,线程也可能切换。这是:

  • CPython 默认每执行 100 个字节码指令,就会检查是否切换线程
  • 这个阈值可以通过 sys.setswitchinterval() 设置(默认是 0.005 秒)。

例如你在两个线程中都做数值加法,这些属于“CPU 密集操作”,但解释器也会定时切换线程执行,防止“一个线程独占”。

如何从根本解决?

方案一:用锁让这三步变成“原子操作”

with lock:
    counter += 1

此时其它线程必须等当前线程加完再来,保证不出错

互斥锁

下面我们来重点学习下互斥锁

什么是互斥锁?

在 Python 中,互斥锁(Mutex Lock) 是一种用于线程同步的机制,用来防止多个线程同时访问同一个共享资源,从而避免出现**数据竞争(竞态条件)**的问题。

互斥锁 = 一把锁,任何时刻只能一个线程持有,其他线程只能等它释放。

一般会将锁加到对共享数据资源进行操作的代码上

在 Python 中怎么用互斥锁?

import threading

lock = threading.Lock()

def safe_increment():
    global counter
    for _ in range(100000):
        with lock:  # 获取锁,执行完自动释放
            counter += 1

等价的写法(更原始):

lock.acquire()
try:
    counter += 1
finally:
    lock.release()

🔸 acquire():尝试获取锁,如果已经被别人拿了,就等着
🔸 release():释放锁,让其他线程可以继续执行
🔸 with lock: 是更优雅的方式(自动释放)

为什么需要互斥锁?

因为线程并发访问共享资源(如变量、文件、数据库)时会出问题:

比如下面这个操作:counter += 1
在底层其实是 3 步:

  • 读取 counter
  • 计算 counter + 1
  • 写回 counter

这不是原子操作,如果两个线程同时做这事,可能“覆盖”彼此的结果,导致最后结果不正确。

互斥锁观察演示:

首先写一个小的循环,观察下

import threading
import time

counter = 0
lock = threading.Lock()

def safe_increment():
    global counter
    for _ in range(5):  # 小次数便于观察
        with lock:
            current = counter
            print(f"[{threading.current_thread().name}] 读取 counter: {current}")
            counter = current + 1
            print(f"[{threading.current_thread().name}] 写入 counter: {counter}")

def main():
    t1 = threading.Thread(target=safe_increment, name="线程1")
    t2 = threading.Thread(target=safe_increment, name="线程2")

    t1.start()
    t2.start()

    t1.join()
    t2.join()

    print(f"最终 counter 值为: {counter}")

if __name__ == "__main__":
    main()
线程

为啥用了互斥锁(Lock)以后,线程1和线程2 不会交错执行

这里的原因是:

原因一:锁虽然在循环内,但线程执行速度太快,交错不明显锁虽然在循环内,但线程执行速度太快,交错不明显

你看到的结果可能是:

  • 线程1连着抢到了几次锁,把循环快速跑完了
  • 线程2后面才抢到锁开始跑

表面上看起来就是“线程没有交替执行”,但实际上是由于:

  • 🔹 CPU 没有及时切换线程(时间片太短)
  • 🔹 操作太快,根本没来得及交错

原因二:GIL 和上下文切换

Python(CPython)有一个 全局解释器锁 GIL(Global Interpreter Lock)

  • 在任何时刻,只有一个线程可以执行 Python 字节码
  • 即使你创建了多个线程,它们不是并行执行,而是交替执行

而操作系统进行线程调度(上下文切换)也有自己的时机,一般:

  • 在 I/O(如 sleep())或
  • 在执行次数到达阈值

如果线程没有触发切换点,它可能就连续运行了多个循环,造成你看到它“几乎独占执行”的假象。

好的,那我们将循环次数大幅度提升下:

import threading
import time

counter = 0
lock = threading.Lock()

def safe_increment():
    global counter
    for _ in range(50):  #  增大次数
        with lock:
            current = counter
            print(f"[{threading.current_thread().name}] 读取 counter: {current}")
            time.sleep(0.1)  # 模拟耗时操作(比如写数据库)
            counter = current + 1
            print(f"[{threading.current_thread().name}] 写入 counter: {counter}")

def main():
    t1 = threading.Thread(target=safe_increment, name="线程1")
    t2 = threading.Thread(target=safe_increment, name="线程2")

    t1.start()
    t2.start()

    t1.join()
    t2.join()

    print(f"最终 counter 值为: {counter}")

if __name__ == "__main__":
    main()
线程

死锁

什么情况下会导致死锁?

我们通过一个典型的“互相等待对方释放资源”的场景来演示死锁。

  • 有两个线程 thread1thread2
  • 有两个锁 lock_alock_b
  • 两个线程尝试以相反的顺序获取锁,这会导致 死锁
import threading
import time

# 创建两个锁
lock_a = threading.Lock()
lock_b = threading.Lock()

def thread1():
    print("线程1 尝试获取 lock_a")
    with lock_a:
        print("线程1 获取了 lock_a")
        time.sleep(1)  # 模拟一些耗时操作
        print("线程1 尝试获取 lock_b")
        with lock_b:
            print("线程1 获取了 lock_b")
    print("线程1 完成")

def thread2():
    print("线程2 尝试获取 lock_b")
    with lock_b:
        print("线程2 获取了 lock_b")
        time.sleep(1)  # 模拟一些耗时操作
        print("线程2 尝试获取 lock_a")
        with lock_a:
            print("线程2 获取了 lock_a")
    print("线程2 完成")

if __name__ == "__main__":
    t1 = threading.Thread(target=thread1)
    t2 = threading.Thread(target=thread2)

    t1.start()
    t2.start()

    t1.join()
    t2.join()

    print("主线程结束")
线程

死锁发生的原因

  1. 线程1 先获得了 lock_a,然后等待 lock_b
  2. 线程2 同时获得了 lock_b,然后等待 lock_a
  3. 两个线程都在等待对方释放自己想要的锁 → 永远卡住,无法继续

如何避免死锁?

  • 始终以相同顺序获取多个锁,比如都先拿 lock_a 再拿 lock_b
  • 使用 try/excepttimeout 避免死锁
  • 用更高级的同步原语(如 threading.Condition

线程和进程的区别

一、关系对比(谁是谁的子集)

对比项说明
线程是进程的组成部分一个进程中可以包含多个线程,线程是进程中的执行单元
进程是资源分配的最小单位系统为进程分配内存、文件句柄、CPU时间等资源
线程是程序执行的最小单位线程只负责执行,资源由所属进程统一管理
线程依赖进程存在没有进程就没有线程,线程必须运行在某个进程中

可以类比成:

  • 进程是“公司”,线程是“员工”
  • 一个公司(进程)可以有多个员工(线程)一起工作,共用办公室(内存资源)

二、区别对比(功能、执行、通信等)

对比维度进程(Process)线程(Thread)
内存空间每个进程有自己独立的内存空间同一进程内的线程共享内存空间
数据共享不共享,全靠 IPC(管道、消息队列、socket)共享内存、全局变量,访问快但不安全
创建开销创建较慢,系统资源开销大创建快速,占资源少
切换开销切换涉及上下文切换、内核态开销大切换速度较快,但仍有调度开销
执行单位程序的执行实例,一个程序可创建多个进程进程的执行分支,一个进程可创建多个线程
崩溃影响崩溃不影响其他进程一个线程崩溃可能导致整个进程崩溃
资源拥有权拥有资源(文件句柄、内存段)不拥有资源,仅执行代码

三、优缺点对比

进程(Process)

优点缺点
稳定性好:互不影响、隔离性强通信复杂、开销大(需 IPC)
崩溃互不干扰,适合多任务并行处理创建/销毁较慢,占用资源更多
更适合 CPU 密集型任务(可充分利用多核)调试和资源管理比线程更复杂

线程(Thread)

优点缺点
创建快速,切换开销小不稳定:一个线程奔溃可能影响整个进程
共享内存,通信效率高共享数据存在竞态条件,需加锁(加锁影响性能,可能死锁)
更适合 I/O 密集型任务或轻量级操作GIL(全局解释器锁)限制 Python 多线程 CPU 并行能力

总结建议:

场景需求建议选择
I/O 密集型任务使用多线程(如爬虫、文件下载)
CPU 密集型任务使用多进程(如图像处理、计算密集任务)
任务之间需要强隔离使用多进程(如不同服务组件)
任务需要频繁共享数据使用多线程(但注意加锁)

发布者:LJH,转发请注明出处:https://www.ljh.cool/42277.html

Like (0)
LJHLJH
Previous 2025年5月22日 下午2:40
Next 2025年5月26日 上午9:54

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注