Python技术迷

详细阐述 threading.local 的作用

我印象特别深,有一次半夜在公司调 bug,一个日志埋点框架,明明请求进来生成的 trace_id 是对的,但打印出来老是串台,A 用户的请求,结果带着 B 用户的 trace_id。那会儿我抓耳挠腮,最后才发现问题出在我用全局变量存东西,后来改成 threading.local 才解决。

为什么需要它

你想啊,在一个 Web 服务里,每个请求就是一个线程。如果你用全局变量存用户 id,那不同线程之间就会互相覆盖,线程 A 设置的,线程 B 一执行就改了。结果就是数据对不上人,日志全乱套。threading.local 的作用就是——给每个线程开一个“独立抽屉”,里面放啥东西都不会影响别的线程。

先上代码,直观一点:

import threading
import time

local_data = threading.local()

defworker(name):
    local_data.user = name
    time.sleep(1)
    print(f"线程 {threading.current_thread().name} 的 user 是 {local_data.user}")

t1 = threading.Thread(target=worker, args=("小明",))
t2 = threading.Thread(target=worker, args=("小红",))

t1.start()
t2.start()
t1.join()
t2.join()

结果就是:

  • t1 始终拿到的是小明
  • t2 始终拿到的是小红

虽然 local_data 是同一个对象,但它在不同线程里“分身”了。

如果不用 threading.local 会怎样

咱们看下对比实验:

import threading
import time

global_user = None

defworker(name):
global global_user
    global_user = name
    time.sleep(1)
    print(f"线程 {threading.current_thread().name} 的 user 是 {global_user}")

t1 = threading.Thread(target=worker, args=("小明",))
t2 = threading.Thread(target=worker, args=("小红",))

t1.start()
t2.start()
t1.join()
t2.join()

你大概率会看到一个线程打印的结果错乱,比如两个线程都显示成了“小红”,因为最后全局变量被覆盖了。这个时候,trace_id、数据库连接之类的东西要是放这里,完蛋。

threading.local 的原理

其实没啥神秘的。Python 底层给每个线程维护了一个字典,threading.local 就是帮你自动把属性存在当前线程的字典里。所以你看到的是一个对象,实际上内部是按线程分开的。

实际应用场景

  • 数据库连接管理:每个线程独立持有一个连接,避免事务串掉。
  • 日志 trace_id:贯穿一次请求的日志上下文,不串台。
  • 请求上下文:像 Flask 里的 g,其实底层就是靠线程本地存储。

比如来个日志上下文的例子:

import threading
import uuid
import time

local_ctx = threading.local()

defhandle_request():
    local_ctx.trace_id = str(uuid.uuid4())
    log("开始处理请求")
    time.sleep(0.5)
    log("处理结束")

deflog(message):
    print(f"[{threading.current_thread().name}][trace_id={local_ctx.trace_id}] {message}")

threads = [threading.Thread(target=handle_request) for _ in range(3)]
for t in threads:
    t.start()
for t in threads:
    t.join()

你会发现,每个线程的 trace_id 都不一样,而且一次请求的日志 trace_id 一致。要是全局变量存,这里直接乱套。

注意点

  • 如果你用线程池(比如 concurrent.futures),线程会复用,那 threading.local 里的数据可能“残留”。所以最好请求结束时清理一下。
  • 在异步环境(asyncio)下,这玩意就不行了,需要用 contextvars。

所以一句话总结:threading.local 就是解决“多线程共享全局变量导致数据串台”的利器。写多线程代码时,不要再拿全局变量硬顶了,乖乖用它,少掉好几个深夜背锅的 bug。

-END-

我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html

🔥虎哥私藏精品🔥

虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB,点击下方公众号回复关键字 python 全部免费领