多线程编程
本教程共 70 篇 · 第 56 篇 · 更新于 2026-07-22 · 约 4 分钟阅读
56. 多线程编程
本节目标:学会用
threading模块创建线程,理解锁的作用,掌握ThreadLocal的用法。
多进程好比开几家分店,各管各的账;多线程则是在同一家店里多请几个店员,共享同一套库存。线程更轻量,切换更快,但也带来了数据竞争的风险。
启动线程
Python 标准库提供两个线程模块:底层的 _thread 和高层的 threading。后者封装得更友好,平时写代码直接用 threading 就行。
import threading
import time
def worker(name, delay):
for i in range(3):
time.sleep(delay)
print(f"[{name}] 执行第 {i+1} 次")
t1 = threading.Thread(target=worker, args=('线程A', 1))
t2 = threading.Thread(target=worker, args=('线程B', 1.5))
t1.start()
t2.start()
t1.join()
t2.join()
print("全部线程结束")
threading.current_thread() 可以拿到当前正在执行的线程对象。主线程的名字固定叫 MainThread,子线程默认叫 Thread-1、Thread-2,你可以在创建时通过 name 参数自定义。
用类封装线程
除了传 target 函数,也可以继承 threading.Thread,重写 run 方法:
import threading
class MyThread(threading.Thread):
def __init__(self, name):
super().__init__(name=name)
self.count = 0
def run(self):
for _ in range(3):
self.count += 1
print(f"{self.name}: {self.count}")
t = MyThread('自定义线程')
t.start()
t.join()
这种方式适合线程逻辑比较复杂、需要维护自身状态的场景。
锁:防止数据打架
多个线程同时读写同一个变量,结果可能出乎意料。看下面的例子:
import threading
balance = 0
def change(n):
global balance
for _ in range(100000):
balance += n
balance -= n
t1 = threading.Thread(target=change, args=(5,))
t2 = threading.Thread(target=change, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance) # 大概率不是 0
balance += n 看起来是一行代码,实际分解成「读、改、写」三步。两个线程穿插执行,就会互相覆盖,最后结果错乱。这就是典型的数据竞争。
解决办法是给关键代码上锁:
lock = threading.Lock()
def change_safe(n):
global balance
for _ in range(100000):
lock.acquire()
try:
balance += n
balance -= n
finally:
lock.release()
acquire() 拿不到锁时会阻塞等待,release() 释放后其他线程才能继续。为了防漏释放,一定记得把业务代码包在 try...finally 里。
Tip更 Pythonic 的写法是用上下文管理器:
with lock:,离开作用域自动释放,省去手动release()的麻烦。
def change_safe(n):
global balance
for _ in range(100000):
with lock:
balance += n
balance -= n
ThreadLocal:线程的私人储物柜
多线程共享全局变量很方便,但有时候每个线程需要自己的独立数据。比如 Web 服务器处理多个请求时,每个线程要保存当前用户的信息。
一种笨办法是把数据当成参数一层层往下传。函数调用链一长,参数列表会爆炸。另一种办法是用全局字典,以线程 ID 为 key 存取。这能行,但代码不好看。
threading.local() 提供了一种优雅的替代方案:
import threading
local_data = threading.local()
def process_user(name):
local_data.user = name
step1()
step2()
def step1():
print(f"步骤1,当前用户: {local_data.user}")
def step2():
print(f"步骤2,当前用户: {local_data.user}")
t1 = threading.Thread(target=process_user, args=('Alice',))
t2 = threading.Thread(target=process_user, args=('Bob',))
t1.start()
t2.start()
t1.join()
t2.join()
local_data 是全局对象,但每个线程读写到的 user 属性都是自己的副本,互不干扰。你可以把它理解成一个「自动按线程分区的字典」。
NoteThreadLocal 的变量在线程结束后会被回收,不用手动清理。但它只在当前线程内有效,不能跨线程传递数据。
小结
threading.Thread启动线程简单直接,继承重写run适合复杂场景。- 共享变量必须加锁保护,
Lock是最基础的同步工具。 ThreadLocal为每个线程提供独立存储空间,避免参数层层传递。- 多线程虽然方便,但受 GIL 限制,同一时刻只有一个线程在执行 Python 字节码。
来源:参考了 runoob「Python3 多线程/threading 模块」、liaoxuefeng「14.2. 多线程/14.3. ThreadLocal」等,改写后所得。