Python threading.local 的 slots:对象叫线程本地,为什么另一个线程改的字段仍然传了回来

前天 3阅读

一个继承threading.local的请求上下文类,原来把request_id存成普通属性。后来为了明确字段,又加入__slots__,却出现不同线程互相改值的现象。这不是线程本地机制偶尔失效,而是普通属性字典和槽位用了不同的存储路径。

下面只启动一个短暂工作线程,主线程通过join等它结束后再检查结果,不依赖sleep碰运气。保存为demo.py,运行python3 demo.py;已在Linux、CPython 3.12.14执行。Queue把工作线程观察到的值带回主线程,输出统一由主线程打印。

Python threading.local 的 slots:对象叫线程本地,为什么另一个线程改的字段仍然传了回来

AI模型生成概念图:两张工作台拥有各自的区域,却连接着同一条上方横杆,用于比喻普通线程属性与共享槽位的区别。

让两类字段接受完全相同的赋值

RequestState只有shared_slot被写进__slots__,request_id仍进入线程本地实例字典。主线程把两者都设为main;工作线程先观察,再把两者都设为worker。若只检查赋值能否成功,两类字段看起来毫无差别,因此最后还必须回到原线程读一次。

完整可运行程序

from queue import Queue
from threading import Thread, local

class RequestState(local):
    __slots__ = ("shared_slot",)

state = RequestState()
state.request_id = "main"
state.shared_slot = "main"
observations = Queue()

def worker():
    observations.put((
        hasattr(state, "request_id"),
        state.shared_slot,
    ))
    state.request_id = "worker"
    state.shared_slot = "worker"
    observations.put((state.request_id, state.shared_slot))

thread = Thread(target=worker)
thread.start()
thread.join()
assert observations.qsize() == 2
before = observations.get_nowait()
after = observations.get_nowait()
assert before == (False, "main")
assert after == ("worker", "worker")
assert state.request_id == "main"
assert state.shared_slot == "worker"
assert state.__dict__ == {"request_id": "main"}
print("worker before:", before)
print("worker after:", after)
print("main after:", state.request_id, state.shared_slot)
print("main dictionary:", state.__dict__)

本次实际输出

worker before: (False, 'main')
worker after: ('worker', 'worker')
main after: main worker
main dictionary: {'request_id': 'main'}

观察值穿过了哪条边界

worker before中的False说明新线程没有主线程的request_id;同一元组中的main却说明它已经看见主线程设定的shared_slot。工作线程完成赋值后,两个字段都读到worker,这是它自己的观察。

真正区分两者的是main after: main worker。主线程的普通属性仍是main,槽位字段已经变为worker。最后的字典输出只含request_id,也说明shared_slot没有放进这份每线程字典。这四个观察合在一起,才足以定位共享来自哪里。

不要用slots声明线程隔离字段

如果一个字段应该跟随当前线程,最直接的修正是让它像request_id一样通过普通实例属性存储,而不是放进local子类的__slots__。把值改成不可变字符串也解决不了问题:共享的是保存该值的位置,两个线程仍会覆盖它。

若确实需要进程内共享状态,应把它放进明确的共享对象,并按读写协议同步。本文的join只是为教学安排确定的先后顺序,不会把共享槽位变成线程本地,更不是并发访问时的通用锁。

线程本地也不等于请求本地

本例每个线程只承担一次工作,因而线程与任务边界恰好重合。线程池可能重复使用同一个线程,普通local属性会跟随那个线程留下。处理请求时仍需在适当的开始和结束位置设置、清理上下文。

也不要把同一线程中的多个协程当成独立线程。需要按异步任务传播上下文时,应选与任务模型匹配的机制。排查隔离问题,先写清希望隔离的是线程、请求还是任务,再检查字段实际落在哪一类存储中。

参考资料

资料核验日期:2026年10月2日。代码输出来自本文固定输入的本地实跑。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。