Skip to main content

Python 进程与线程

Python 线程的 ThreadLocal 对象

ThreadLocal 是 Python threading 模块提供的一种机制,用于在多线程环境中为每个线程维护独立的变量副本。每个线程对 ThreadLocal 对象的操作(如设置或获取值)只会影响该线程的副本,线程之间互不干扰。这非常适合需要线程隔离的场景,例如存储线程独有的配置、上下文或状态。

核心特点

  • 线程隔离:每个线程有自己的变量副本。
  • 简化代码:无需显式传递线程特定的数据。
  • 典型用途:数据库连接、用户会话、日志上下文等。

ThreadLocal 的基本使用

threading.local() 创建一个 ThreadLocal 对象,线程可以通过其属性存储和访问数据。以下是一个简单示例,展示如何在多个线程中使用 ThreadLocal

import threading

# 创建 ThreadLocal 对象
local_data = threading.local()

def print_thread_data(name):
    # 为当前线程设置数据
    local_data.value = name
    print(f"线程 {name} 的值: {local_data.value}")

# 创建两个线程
thread1 = threading.Thread(target=print_thread_data, args=("线程1",))
thread2 = threading.Thread(target=print_thread_data, args=("线程2",))

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

# 等待线程结束
thread1.join()
thread2.join()

输出(顺序可能不同):

线程 线程1 的值: 线程1
线程 线程2 的值: 线程2
  • local_dataThreadLocal 对象,每个线程的 local_data.value 是独立的。
  • 线程1 和线程2 各自设置并读取自己的 value,互不影响。

ThreadLocal 的工作原理

ThreadLocal 内部维护了一个线程 ID 到数据副本的映射。当线程访问 ThreadLocal 对象的属性时,Python 会根据当前线程的 ID 查找或创建对应的数据副本。这种机制确保了线程隔离。

关键点

  • ThreadLocal 不存储全局共享数据,而是为每个线程分配独立存储空间。
  • 属性访问(如 local_data.value)是线程安全的,无需额外加锁。

常见使用场景

存储线程上下文

ThreadLocal 常用于保存线程特定的上下文,例如用户 ID 或请求 ID。以下示例模拟一个多线程服务器,为每个线程保存唯一的请求 ID。

import threading
import uuid

# 创建 ThreadLocal 对象
request_context = threading.local()

def process_request(request_id):
    # 设置线程的请求 ID
    request_context.request_id = request_id
    print(f"线程 {threading.current_thread().name} 处理请求: {request_context.request_id}")
    # 模拟处理
    perform_task()

def perform_task():
    # 访问当前线程的请求 ID
    print(f"任务执行中,请求 ID: {request_context.request_id}")

# 创建线程模拟请求处理
thread1 = threading.Thread(target=process_request, args=(str(uuid.uuid4()),), name="Thread-1")
thread2 = threading.Thread(target=process_request, args=(str(uuid.uuid4()),), name="Thread-2")

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

输出(请求 ID 为随机 UUID):

线程 Thread-1 处理请求: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
任务执行中,请求 ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
线程 Thread-2 处理请求: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
任务执行中,请求 ID: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
  • 每个线程的 request_context.request_id 是独立的,perform_task 函数无需传递参数即可访问。

管理数据库连接

在多线程应用中,每个线程可能需要独立的数据库连接。ThreadLocal 可用于管理这些连接。

import threading
import sqlite3

# 创建 ThreadLocal 对象
db_context = threading.local()

def get_db_connection():
    # 如果当前线程没有连接,则创建新连接
    if not hasattr(db_context, 'conn'):
        db_context.conn = sqlite3.connect(':memory:')
        db_context.conn.execute('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)')
    return db_context.conn

def add_user(name):
    conn = get_db_connection()
    conn.execute('INSERT INTO users (name) VALUES (?)', (name,))
    conn.commit()
    print(f"线程 {threading.current_thread().name} 添加用户: {name}")

def query_users():
    conn = get_db_connection()
    cursor = conn.execute('SELECT name FROM users')
    names = [row[0] for row in cursor.fetchall()]
    print(f"线程 {threading.current_thread().name} 查询用户: {names}")

# 创建线程
thread1 = threading.Thread(target=lambda: [add_user("Alice"), query_users()], name="Thread-1")
thread2 = threading.Thread(target=lambda: [add_user("Bob"), query_users()], name="Thread-2")

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

输出

线程 Thread-1 添加用户: Alice
线程 Thread-1 查询用户: ['Alice']
线程 Thread-2 添加用户: Bob
线程 Thread-2 查询用户: ['Bob']

说明

  • 每个线程有自己的数据库连接,互不干扰。
  • get_db_connection 确保线程首次访问时创建连接。

注意事项与局限性

动态属性

ThreadLocal 对象的属性可以动态创建,但需确保属性名在所有线程中一致,否则可能导致逻辑错误。

内存管理

ThreadLocal 不会自动清理线程的数据副本。如果线程退出后未清理,可能导致内存泄漏。以下示例展示如何手动清理:

import threading

# 创建 ThreadLocal 对象
local_data = threading.local()

def worker():
    local_data.value = "一些数据"
    print(f"线程 {threading.current_thread().name} 设置值: {local_data.value}")
    # 清理
    if hasattr(local_data, 'value'):
        del local_data.value

thread = threading.Thread(target=worker, name="Worker")
thread.start()
thread.join()
print("主线程尝试访问值:", getattr(local_data, 'value', '无值'))

输出

线程 Worker 设置值: 一些数据
主线程尝试访问值: 无值

不适用于线程池

在线程池中,线程可能被复用,ThreadLocal 的数据可能残留。建议在任务开始和结束时显式设置和清理数据。

高级应用:结合装饰器

可以使用装饰器简化 ThreadLocal 的上下文管理。以下示例展示如何自动设置请求上下文。

import threading
import uuid
from functools import wraps

# 创建 ThreadLocal 对象
request_context = threading.local()

def with_request_context(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        request_context.request_id = str(uuid.uuid4())
        try:
            return func(*args, **kwargs)
        finally:
            # 清理上下文
            if hasattr(request_context, 'request_id'):
                del request_context.request_id
    return wrapper

@with_request_context
def process_task():
    print(f"处理任务,请求 ID: {request_context.request_id}")

# 创建线程
thread1 = threading.Thread(target=process_task, name="Thread-1")
thread2 = threading.Thread(target=process_task, name="Thread-2")

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

输出

处理任务,请求 ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
处理任务,请求 ID: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
  • 装饰器 with_request_context 自动为每个线程设置和清理请求 ID。