在这里插入图片描述

从入门到实战:彻底讲透 Python threading 的工作原理、正确用法与避坑指南

很多人第一次接触 Python 多线程,都会有一种“会用,但没真正懂”的感觉。
明明开了好几个线程,程序却不一定更快;
明明逻辑很简单,却偶尔出现莫名其妙的数据错误;
明明线程都 start() 了,程序还是卡住不退出。

这篇文章就来把 Python threading 一次讲透。
不只是“怎么写”,更重要的是“为什么这样写”。


目录


一、为什么要学 threading

在 Python 里,threading 是最经典、最常见的并发编程工具之一。

很多业务场景里,它都特别实用,比如:

  • 🌐 同时发起多个网络请求
  • 📁 同时处理多个文件读写任务
  • 🖥️ GUI 程序中避免界面卡死
  • 🧾 日志上报、埋点、消息推送等后台任务
  • 🔄 生产者-消费者模型
  • 🚦 控制资源访问顺序或并发数量

一句话概括:

只要你的任务“多数时间在等待”,多线程通常就有发挥空间。

这里的“等待”包括:

  • 等网络响应
  • 等磁盘 IO
  • 等数据库返回
  • 等第三方接口
  • 等队列消息
  • 等用户输入

这也是理解 threading 的第一把钥匙。


二、什么是线程?和进程有什么区别?

先别急着写代码,先把概念捋顺。

1. 进程是什么

进程(Process)可以理解为:

一个正在运行的程序实例。

比如你双击启动一个 Python 程序,操作系统就会为它创建一个进程。这个进程有自己独立的:

  • 内存空间
  • 文件描述符
  • 资源上下文
  • 地址空间

2. 线程是什么

线程(Thread)是:

进程内部的执行单元。

一个进程里可以有多个线程,这些线程共享同一个进程的大部分资源,比如:

  • 代码段
  • 堆内存
  • 全局变量
  • 打开的文件资源

但每个线程也有自己独立的:

  • 程序计数器
  • 寄存器上下文
  • 栈空间

3. 进程 vs 线程

对比项进程线程
资源隔离
创建成本
切换成本较低
数据共享麻烦容易
安全性更高更容易出错
通信方式IPC直接共享内存

4. 一个非常形象的类比

可以把进程想象成一家工厂,把线程想象成工厂里的工人:

  • 工厂(进程)有自己的厂房和设备
  • 工人(线程)共享这些设备
  • 工人之间协作更方便
  • 但如果没有管理好,也更容易“撞车”

所以线程的核心特点是:

轻量、共享、协作方便,但需要额外解决同步问题。


三、Python 为什么需要 threading

因为现实世界里,很多程序都不是单纯地“计算”,而是在做大量等待。

举个例子:

你要请求 20 个接口,每个接口平均等待 1 秒。

如果串行执行,大概需要:

20 * 1 = 20

如果你同时起 20 个线程去请求,那么整体耗时会接近:

1 秒多一点

因为这些线程大部分时间都在“等响应”,CPU 正好可以切换去执行别的线程。

所以:

对于 IO 密集型任务,多线程非常有价值。


四、先建立正确认知:Python 多线程不一定让程序更快

这是很多人最容易误解的一点。

1. 多线程不是“万能加速器”

很多人学完多线程之后,会下意识觉得:

“我把任务拆成多个线程,程序就会更快。”

这句话并不总成立。

Python 中的 threading 更适合:

  • IO 密集型任务
  • 等待时间远多于计算时间的任务

而不适合:

  • 大量纯计算任务
  • CPU 持续满负载的任务

2. 为什么?

因为 Python 里有一个经典机制:GIL(全局解释器锁)

后面我们会详细讲,但你现在先记住一句:

在 CPython 中,同一时刻只能有一个线程执行 Python 字节码。

这意味着:

  • 多线程做 IO,通常有效
  • 多线程做 CPU 密集计算,通常没明显收益,甚至更慢

五、threading 模块快速上手

Python 标准库自带 threading,不需要安装。

import threading

1. 最简单的线程示例

import threading
import time


def worker():
    print(f"线程 {threading.current_thread().name} 开始执行")
    time.sleep(2)
    print(f"线程 {threading.current_thread().name} 执行结束")


t = threading.Thread(target=worker, name="Worker-1")
t.start()

print("主线程继续往下执行")
t.join()
print("主线程结束")

输出大概类似:

线程 Worker-1 开始执行
主线程继续往下执行
线程 Worker-1 执行结束
主线程结束

这里发生了什么?

  • Thread(...) 创建线程对象
  • target=worker 指定线程要执行的函数
  • start() 启动线程
  • 主线程不会等它,继续往下执行
  • join() 表示主线程等待子线程执行完

2. 使用函数创建线程

这是最常见、最直接的方式。

import threading


def task(task_id, username):
    print(f"任务ID: {task_id}, 用户: {username}, 当前线程: {threading.current_thread().name}")


t = threading.Thread(target=task, args=(1001, "alex"), name="TaskThread")
t.start()
t.join()

参数说明

参数作用
target线程执行的目标函数
args位置参数元组
kwargs关键字参数字典
name线程名称
daemon是否为守护线程

比如:

t = threading.Thread(
    target=task,
    args=(1001, "alex"),
    daemon=False,
    name="TaskThread"
)

3. 继承 Thread 类创建线程

如果你的线程逻辑比较复杂,或者希望把线程的状态封装起来,继承 Thread 是更好的方式。

import threading
import time


class DownloadThread(threading.Thread):
    def __init__(self, url):
        super().__init__()
        self.url = url

    def run(self):
        print(f"{self.name} 开始下载: {self.url}")
        time.sleep(2)
        print(f"{self.name} 下载完成: {self.url}")


t1 = DownloadThread("https://example.com/a")
t2 = DownloadThread("https://example.com/b")

t1.start()
t2.start()

t1.join()
t2.join()

什么时候适合继承?

适合这些场景:

  • 线程逻辑本身就是一个独立对象
  • 线程有状态,比如任务 ID、URL、结果、异常信息
  • 想更面向对象地组织代码

什么时候不适合?

如果只是简单调用一个函数,那就没必要为了“面向对象”而强行继承,反而更重。


六、线程对象的核心属性与方法

下面这些是日常写多线程最常用的内容。

名称类型说明
start()方法启动线程
run()方法线程执行逻辑
join()方法等待线程结束
is_alive()方法线程是否仍在运行
name属性线程名
daemon属性是否为守护线程
ident属性线程标识符
native_id属性OS 层线程 ID(Python 3.8+)

示例:

import threading
import time


def worker():
    time.sleep(1)


t = threading.Thread(target=worker, name="DemoThread")
print(t.is_alive())   # False

t.start()
print(t.is_alive())   # True

t.join()
print(t.is_alive())   # False

七、线程的执行流程:start()run()join() 到底是什么关系?

这是一个很基础,但非常容易被忽视的问题。

1. start() 是启动线程

调用 start() 后,Python 会真正创建一个新的线程,并在新线程里调用 run()

t.start()

2. run() 是线程执行体

默认情况下,run() 会去执行你传入的 target 函数。

如果你是继承 Thread,那通常就是重写这个方法。

3. join() 是等待线程结束

它不会“启动线程”,而是让当前线程阻塞,直到目标线程结束。


4. 为什么不能直接调 run()

因为直接调用:

t.run()

不是多线程执行,而只是普通函数调用,代码仍然在当前线程里跑。

这点特别重要 ⚠️

来看示例:

import threading


def worker():
    print(f"当前线程: {threading.current_thread().name}")


t = threading.Thread(target=worker, name="SubThread")

t.run()    # 普通方法调用,不会启动新线程

输出很可能是:

当前线程: MainThread

而不是 SubThread


八、线程共享数据:方便,但危险

线程最大的优势之一,就是共享同一进程的内存空间。

例如:

import threading

counter = 0


def worker():
    global counter
    counter += 1


threads = []
for _ in range(100):
    t = threading.Thread(target=worker)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print(counter)

你可能以为结果一定是 100,但实际上不一定

为什么?

因为:

counter += 1

看起来像“一步”,实际上不是原子操作。

它大致可以拆成:

  1. 读取 counter
  2. 计算 counter + 1
  3. 写回 counter

如果两个线程交叉执行,就可能发生数据覆盖。

这就是经典的:竞态条件(Race Condition)


九、线程安全到底是什么?

所谓线程安全,可以简单理解为:

多个线程同时访问某段代码或某份数据时,结果仍然是正确且可预期的。

线程不安全,通常会出现这些问题:

  • 结果偶发错误
  • 同样的代码多跑几次结果不一样
  • 某些 bug 很难复现
  • 程序偶尔死锁、卡住、数据错乱

一个经典错误示例

import threading

counter = 0


def add():
    global counter
    for _ in range(100000):
        counter += 1


threads = [threading.Thread(target=add) for _ in range(2)]

for t in threads:
    t.start()

for t in threads:
    t.join()

print(counter)

理论上结果应该是:

200000

但实际可能小于这个值。

这就说明:

共享可变数据 + 无同步控制 = 高风险。


十、锁:Lock 的本质与使用场景

想解决共享数据竞争问题,最基础的办法就是:加锁

1. 什么是锁

锁的本质是:

让某一段代码在同一时刻只允许一个线程进入。

这段代码通常叫:临界区(Critical Section)

2. Lock 基本用法

import threading

counter = 0
lock = threading.Lock()


def add():
    global counter
    for _ in range(100000):
        with lock:
            counter += 1


threads = [threading.Thread(target=add) for _ in range(2)]

for t in threads:
    t.start()

for t in threads:
    t.join()

print(counter)

这时候结果就稳定了。


3. acquire() / release() 写法

除了 with,也可以手动写:

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

推荐结论

✅ 优先使用 with lock:
因为更安全、更简洁,也不容易忘记释放锁。


4. 锁会带来什么代价?

任何同步机制都不是免费的。

加锁会带来:

  • 上下文切换开销
  • 等待开销
  • 代码复杂度上升
  • 死锁风险

所以一个很重要的原则是:

锁要加在必要的位置,而且范围尽可能小。

不好的写法

with lock:
    do_something()
    do_other_thing()
    time.sleep(2)

如果 sleep(2) 没必要放在锁里,这就会导致别的线程白白等待。

更合理的写法

with lock:
    update_shared_data()

time.sleep(2)

十一、可重入锁:RLock 解决什么问题?

普通 Lock 有一个特点:

  • 同一个线程拿到锁之后
  • 如果它自己再次申请这把锁
  • 会把自己卡住

示例:

import threading

lock = threading.Lock()


def func_a():
    with lock:
        func_b()


def func_b():
    with lock:
        print("执行 func_b")

这段代码可能死锁。

因为 func_a() 已经拿了锁,进入 func_b() 后又申请同一把锁,而普通锁不允许同一线程重复获取。

这时候就要用 RLock(Reentrant Lock,可重入锁)。

import threading

lock = threading.RLock()


def func_a():
    with lock:
        func_b()


def func_b():
    with lock:
        print("执行 func_b")

LockRLock 对比

对比项LockRLock
同一线程重复加锁不允许允许
性能略高略低
使用场景普通互斥嵌套调用、递归调用

实战建议

  • 默认优先用 Lock
  • 确实存在“同线程重复进入锁区”时再考虑 RLock

十二、条件变量:Condition 如何实现“等待通知”

当线程之间不是单纯抢资源,而是需要“等某个条件成立”时,Lock 就不够优雅了。

这时候可以用 Condition

1. 典型场景

  • 队列为空,消费者要等待
  • 资源未准备好,工作线程要等待
  • 某个状态满足后,再通知其他线程继续执行

2. 基本模型

  • 一个线程 wait()
  • 另一个线程 notify()notify_all()

3. 示例

import threading
import time

condition = threading.Condition()
ready = False


def consumer():
    global ready
    with condition:
        while not ready:
            print("消费者:条件不满足,进入等待")
            condition.wait()
        print("消费者:条件满足,开始执行")


def producer():
    global ready
    time.sleep(2)
    with condition:
        ready = True
        print("生产者:条件已准备好,发出通知")
        condition.notify()


t1 = threading.Thread(target=consumer)
t2 = threading.Thread(target=producer)

t1.start()
t2.start()

t1.join()
t2.join()

4. 为什么 wait() 外面要配合 while

很多初学者会写成:

if not ready:
    condition.wait()

但更推荐:

while not ready:
    condition.wait()

原因有两个:

  • 可能被伪唤醒
  • 被唤醒后,条件也不一定仍然成立

所以这是经典写法:

with condition:
    while not 条件:
        condition.wait()

十三、信号量:Semaphore 适合控制并发数量

有些场景不是“只能一个线程进入”,而是“最多允许 N 个线程同时进入”。

比如:

  • 同时最多允许 3 个线程下载
  • 同时最多允许 10 个线程调用数据库
  • 同时最多允许 5 个线程访问某个第三方接口

这时用 Semaphore 非常合适。

示例

import threading
import time

semaphore = threading.Semaphore(3)


def worker(i):
    with semaphore:
        print(f"线程 {i} 进入执行")
        time.sleep(2)
        print(f"线程 {i} 执行结束")


threads = []
for i in range(10):
    t = threading.Thread(target=worker, args=(i,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

效果

虽然开了 10 个线程,但任意时刻只有 3 个线程真正进入执行区。


十四、事件对象:Event 做线程间通知非常好用

如果你的需求只是:

  • 一个线程等通知
  • 另一个线程发通知
  • 不需要复杂的锁条件管理

那么 Event 往往比 Condition 更轻量、更直观。

常用方法

方法说明
set()设置事件为已触发
clear()重置事件
wait()等待事件触发
is_set()判断是否已触发

示例

import threading
import time

event = threading.Event()


def worker():
    print("工作线程:等待开始信号")
    event.wait()
    print("工作线程:收到信号,开始执行")


def controller():
    time.sleep(2)
    print("控制线程:发出开始信号")
    event.set()


t1 = threading.Thread(target=worker)
t2 = threading.Thread(target=controller)

t1.start()
t2.start()

t1.join()
t2.join()

适用场景

  • 程序启动协调
  • 多线程统一开始
  • 停止信号广播
  • 某个资源准备完成后通知多个线程

十五、队列:线程通信最推荐的方式

如果你真的要在多线程之间传数据,首选通常不是共享列表或字典,而是 queue.Queue

为什么?

因为它天然线程安全。

1. 为什么推荐队列

相比手动管理共享变量:

  • 更安全
  • 更清晰
  • 更符合生产者-消费者模型
  • 不容易写出难排查的并发 bug

2. 基本示例

import threading
import queue
import time

q = queue.Queue()


def producer():
    for i in range(5):
        print(f"生产者放入: {i}")
        q.put(i)
        time.sleep(1)
    q.put(None)  # 结束标记


def consumer():
    while True:
        item = q.get()
        if item is None:
            print("消费者结束")
            break
        print(f"消费者取出: {item}")
        q.task_done()


t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)

t1.start()
t2.start()

t1.join()
t2.join()

3. Queue 常用方法

方法说明
put(item)放入数据
get()取出数据
task_done()标记任务处理完成
join()等待所有任务完成
put_nowait()非阻塞放入
get_nowait()非阻塞取出

4. 一个核心建议

线程之间通信,优先考虑 Queue

这是非常重要的工程经验。

因为你越少共享可变状态,线程安全问题就越少。


十六、定时器:Timer 的使用方式

Timer 可以理解为“延迟执行一次的线程”。

示例

import threading


def say_hello():
    print("3 秒后执行了这个函数")


timer = threading.Timer(3, say_hello)
timer.start()

取消定时任务

timer.cancel()

适合场景

  • 延迟执行某个函数
  • 超时控制
  • 定时提醒
  • 简单一次性触发任务

注意:Timer 本质上还是线程,不适合拿来做复杂调度系统。


十七、守护线程:daemon=True 到底意味着什么?

很多人看到 daemon=True,大概知道“主线程结束它也会结束”,但理解不够深。

1. 什么是守护线程

守护线程的含义是:

当程序中只剩守护线程时,整个进程会直接退出。

也就是说,守护线程不会阻止程序结束。

2. 示例

import threading
import time


def worker():
    while True:
        print("后台线程运行中...")
        time.sleep(1)


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

time.sleep(3)
print("主线程结束")

运行后你会看到:

  • 后台线程打印几次
  • 主线程结束后,整个程序直接退出
  • 守护线程不会继续保活

3. 什么时候适合用守护线程

适合:

  • 后台日志刷新
  • 心跳检测
  • 非关键监控线程
  • 程序退出时可直接丢弃的任务

不适合:

  • 必须执行完的写库、落盘、转账、关键业务逻辑

一个实战原则

只把“可以随时中断也无所谓”的线程设为守护线程。


十八、GIL 是什么?为什么它总和多线程一起出现?

这是 Python 多线程绕不过去的话题。

1. GIL 全称

GIL = Global Interpreter Lock
中文常叫:全局解释器锁

2. 它做了什么

在 CPython 解释器中,GIL 保证:

同一时刻只有一个线程执行 Python 字节码。

这意味着即使你开了多个线程,它们也不是同时执行 Python 代码,而是轮流拿到 GIL 去执行。

3. 那为什么 IO 密集型任务仍然有效?

因为线程在等待 IO 时,会释放 GIL。

例如:

  • 网络请求等待响应
  • sleep()
  • 文件读写等待磁盘
  • 某些 C 扩展内部阻塞调用

线程 A 等待 IO 时,线程 B 就有机会运行。

所以对于 IO 型任务,多线程依然能提升整体吞吐。

4. 为什么 CPU 密集型任务不适合?

因为 CPU 密集任务一直在执行 Python 计算逻辑,线程会频繁竞争 GIL。

结果可能是:

  • 不能真正并行
  • 上下文切换开销变大
  • 性能不升反降

5. 一张表彻底记住

任务类型使用 threading 效果
网络请求很适合
文件 IO很适合
数据库访问很适合
批量接口调用很适合
图像纯计算不太适合
大规模数值运算不太适合
CPU 密集循环通常不适合

十九、threading 适合做什么,不适合做什么

适合的场景 ✅

  • 爬虫抓取多个页面
  • 多接口并发请求
  • 批量处理文件
  • 日志/埋点异步写入
  • 消费队列任务
  • GUI 程序后台执行任务
  • 需要共享内存、协作方便的轻量并发任务

不适合的场景 ❌

  • 大规模科学计算
  • 视频编码
  • 图像处理中的纯 CPU 计算
  • 复杂并行计算任务
  • 想依靠多线程“榨满多核 CPU”的场景

这种情况该怎么办?

  • CPU 密集:优先考虑 multiprocessing
  • 高并发网络:优先考虑 asyncio
  • 简化线程池管理:考虑 concurrent.futures.ThreadPoolExecutor

二十、实战案例一:多线程批量下载网页内容

先来一个非常典型的 IO 密集型案例。

串行版本

import requests
import time

urls = [
    "https://example.com",
    "https://httpbin.org/delay/1",
    "https://httpbin.org/delay/1",
    "https://httpbin.org/delay/1",
]

start = time.time()

for url in urls:
    response = requests.get(url, timeout=5)
    print(url, response.status_code)

print("总耗时:", time.time() - start)

如果每个请求都要等 1 秒,总耗时会比较明显。


多线程版本

import threading
import requests
import time

urls = [
    "https://example.com",
    "https://httpbin.org/delay/1",
    "https://httpbin.org/delay/1",
    "https://httpbin.org/delay/1",
]

results = []
lock = threading.Lock()


def fetch(url):
    try:
        response = requests.get(url, timeout=5)
        with lock:
            results.append((url, response.status_code, len(response.text)))
    except Exception as e:
        with lock:
            results.append((url, "ERROR", str(e)))


threads = []
start = time.time()

for url in urls:
    t = threading.Thread(target=fetch, args=(url,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

for item in results:
    print(item)

print("总耗时:", time.time() - start)

为什么这里多线程更有效?

因为每个线程大部分时间都在等待网络返回,而不是一直占用 CPU。


二十一、实战案例二:生产者-消费者模型

这是线程编程里最经典的模型之一。

业务场景

  • 生产者负责生成任务
  • 消费者负责处理任务
  • 中间通过队列传递数据

示例代码

import threading
import queue
import time
import random

task_queue = queue.Queue()


def producer():
    for i in range(10):
        item = f"task-{i}"
        print(f"[生产者] 生产 {item}")
        task_queue.put(item)
        time.sleep(random.uniform(0.1, 0.5))

    # 通知消费者结束
    for _ in range(3):
        task_queue.put(None)


def consumer(worker_id):
    while True:
        item = task_queue.get()
        if item is None:
            print(f"[消费者-{worker_id}] 收到结束信号")
            task_queue.task_done()
            break

        print(f"[消费者-{worker_id}] 正在处理 {item}")
        time.sleep(random.uniform(0.5, 1.5))
        print(f"[消费者-{worker_id}] 完成 {item}")
        task_queue.task_done()


producer_thread = threading.Thread(target=producer)
consumer_threads = [
    threading.Thread(target=consumer, args=(i,))
    for i in range(3)
]

producer_thread.start()
for t in consumer_threads:
    t.start()

producer_thread.join()
task_queue.join()

for t in consumer_threads:
    t.join()

print("所有任务处理完成")

这个模型为什么好?

因为它有几个很强的工程优势:

  • 生产和消费解耦
  • 可以平滑削峰
  • 队列天然线程安全
  • 容易扩展消费者数量
  • 逻辑清晰,可维护性高

这也是生产环境里极常见的一种模式。


二十二、实战案例三:限制最大并发数的爬虫/接口调用器

很多接口都有频率限制,或者你不希望一口气发太多请求。这时候 Semaphore 特别适合。

import threading
import time
import random

semaphore = threading.Semaphore(3)


def call_api(task_id):
    with semaphore:
        print(f"任务 {task_id} 开始请求")
        time.sleep(random.uniform(1, 2))
        print(f"任务 {task_id} 请求结束")


threads = []
for i in range(10):
    t = threading.Thread(target=call_api, args=(i,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

这个方案的价值

  • 线程数可以多
  • 但真正进入核心执行区的并发数被严格限制
  • 很适合第三方接口调用、下载器、数据库连接池外层控制

二十三、常见坑位与排查思路

这部分非常重要。
真正拉开水平差距的,往往不是“会写线程”,而是“出了问题能不能快速定位”。


1. 忘记 join(),主线程提前结束

t.start()
print("主线程结束")

如果不 join(),主线程可能直接退出,导致你误以为线程“没执行完”。

排查思路

  • 检查是否等待了所有子线程
  • 关键流程不要依赖“线程自己跑完”

2. 把 run() 当成 start()

这是新手高频错误。

t.run()

这不是启动线程,而是普通方法调用。

排查思路

  • 看是否真的调用了 start()
  • 打印 threading.current_thread().name 验证线程身份

3. 共享变量未加锁

这类 bug 最烦,因为它常常“偶现”。

排查信号

  • 结果数量偶尔不对
  • 计数器结果偏小
  • 列表长度异常
  • 状态偶发错乱

解决思路

  • 对共享可变状态加锁
  • 或改成消息传递(Queue
  • 尽量减少共享写操作

4. 死锁

死锁常见于:

  • 多把锁交叉获取
  • 忘记释放锁
  • 同一线程重复申请普通 Lock
  • 条件等待与通知逻辑不匹配

经典死锁示例

# 线程 A
with lock1:
    with lock2:
        pass

# 线程 B
with lock2:
    with lock1:
        pass

如果两个线程同时执行,就可能互相等待。

避免死锁的经验

✅ 锁顺序保持一致
✅ 尽量减少锁嵌套
✅ 用 with 自动释放
✅ 能用 Queue 就少用共享锁
✅ 复杂场景给锁获取加超时


5. 守护线程误用

你把关键任务放进守护线程,主线程一结束,任务就直接没了。

典型事故

  • 日志没刷完
  • 文件没写完整
  • 数据库事务没提交
  • 消息没发出去

经验

关键任务线程,不要设成守护线程。


6. 在线程里异常静默丢失

线程里的异常不会总是像主线程那样明显暴露。

建议写法

import threading
import traceback


def worker():
    try:
        1 / 0
    except Exception:
        print(f"{threading.current_thread().name} 发生异常")
        traceback.print_exc()

更好的工程实践

  • 在线程函数最外层统一捕获异常
  • 记录日志
  • 把异常信息存到结果队列
  • 主线程统一收集并处理

7. 线程数量开太多

并不是线程越多越好。

线程过多可能导致:

  • 内存上涨
  • 调度开销增大
  • 上下文切换频繁
  • 资源被打爆
  • 接口方把你限流

一个常见误区

“我有 1000 个任务,就开 1000 个线程。”

这通常不是好主意。

更合理的方式是:

  • 固定数量线程池
  • Queue 投递任务
  • 或者 Semaphore 控制并发

二十四、threadingconcurrent.futures 怎么选?

这个问题非常常见。

1. threading

更底层、更灵活,适合:

  • 你要自己管理线程生命周期
  • 你要用锁、事件、条件变量等同步原语
  • 你要搭建复杂线程协作逻辑

2. ThreadPoolExecutor

更高级、更省心,适合:

  • 一批独立任务并发执行
  • 不想手动管理线程对象
  • 想方便地拿返回值、异常、超时结果

对比表

对比项threadingThreadPoolExecutor
灵活性
上手难度较高
适合复杂同步一般
适合任务池一般很适合
返回值处理自己做内置 Future

经验建议

  • 复杂线程协作:用 threading
  • 简单并发跑任务:优先 ThreadPoolExecutor

二十五、面试高频问题总结

下面这部分,几乎可以当成面试速记卡来背。


1. Python 多线程能并行吗?

答:

  • 对于 IO 密集型任务,能提升并发效率
  • 对于 CPU 密集型任务,在 CPython 下由于 GIL,通常不能真正发挥多核并行优势

2. start()run() 的区别是什么?

答:

  • start() 会启动新线程,并自动调用 run()
  • run() 只是普通方法,直接调用不会创建新线程

3. join() 的作用是什么?

答:

  • 让当前线程等待目标线程执行结束

4. 什么是线程安全?

答:

  • 多个线程同时访问共享资源时,程序结果仍然正确、稳定、可预期

5. LockRLock 的区别?

答:

  • Lock 不能被同一线程重复获取
  • RLock 可以被同一线程重复获取,适合嵌套调用场景

6. ConditionEventSemaphore 分别适合什么?

答:

  • Condition:等待某个条件成立并通知
  • Event:简单线程通知/广播
  • Semaphore:限制同时访问资源的线程数量

7. 为什么 Queue 在线程通信中更推荐?

答:

  • 它是线程安全的
  • 能减少共享变量带来的竞态问题
  • 更符合生产者-消费者模型

8. 什么是守护线程?

答:

  • 守护线程不会阻止进程退出;当只剩守护线程时,进程会直接结束

二十六、一份可落地的最佳实践清单

这部分我建议你收藏起来,写多线程代码时拿出来对照。

多线程开发建议清单

设计层面

  • ✅ 优先判断任务是不是 IO 密集型
  • ✅ 能不用共享状态就不用共享状态
  • ✅ 优先使用 Queue 做线程间通信
  • ✅ 优先限制并发度,而不是无脑开线程

编码层面

  • ✅ 用 start() 启动线程,不要直接调 run()
  • ✅ 用 join() 等待关键线程结束
  • ✅ 用 with lock: 代替手动 acquire/release
  • ✅ 把加锁范围控制到最小
  • ✅ 线程入口统一捕获异常

架构层面

  • ✅ 简单任务并发,优先考虑线程池
  • ✅ CPU 密集任务考虑 multiprocessing
  • ✅ 高并发网络任务考虑 asyncio
  • ✅ 对外部资源访问要有并发上限控制

一张总表:threading 常用工具怎么选

工具作用适用场景
Thread创建线程基础线程任务
Lock互斥访问保护共享数据
RLock可重入互斥嵌套加锁
Condition条件等待/通知等状态变化
Event信号通知开始/停止广播
Semaphore限制并发数下载器、接口限流
Queue线程安全通信生产者-消费者
Timer延迟执行简单定时任务

二十七、总结

threading 真正难的,不是 API 本身,而是并发思维。

你会发现它的学习过程大概分三个阶段:

第一阶段:会用

知道怎么:

  • 创建线程
  • 启动线程
  • 等待线程结束

第二阶段:能写

知道怎么:

  • 用锁保护共享数据
  • 用事件、条件变量做线程协作
  • 用队列做线程间通信

第三阶段:能写对

知道怎么:

  • 分辨任务是否适合多线程
  • 避开 GIL 误区
  • 控制线程数量
  • 排查竞态、死锁、异常丢失等问题
  • 写出更稳定、更工程化的并发代码

最后送你一句很实用的话:

Python 多线程的重点,从来不是“把程序改成多线程”,而是“把适合并发的那部分,安全地并发起来”。

这才是 threading 真正的价值。


附:一个简洁但实用的多线程模板

你在实际开发中,经常可以从下面这个模板起步。

import threading
import queue
import traceback

task_queue = queue.Queue()
result_queue = queue.Queue()


def worker():
    while True:
        item = task_queue.get()
        if item is None:
            task_queue.task_done()
            break

        try:
            # 处理任务
            result = item * 2
            result_queue.put((item, result, None))
        except Exception as e:
            result_queue.put((item, None, traceback.format_exc()))
        finally:
            task_queue.task_done()


# 创建固定数量工作线程
threads = []
for _ in range(4):
    t = threading.Thread(target=worker)
    t.start()
    threads.append(t)

# 投递任务
for i in range(10):
    task_queue.put(i)

# 发送结束信号
for _ in threads:
    task_queue.put(None)

# 等待任务完成
task_queue.join()

# 等待线程结束
for t in threads:
    t.join()

# 获取结果
while not result_queue.empty():
    item, result, err = result_queue.get()
    if err:
        print(f"任务 {item} 失败:\n{err}")
    else:
        print(f"任务 {item} 成功,结果={result}")

这个模板的优点是:

  • 线程数可控
  • 任务通信安全
  • 支持错误收集
  • 容易扩展成生产代码
Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐