理解 Python 中的 ThreadLocal 变量 (一)

    xiaoxiao2023-10-14  145

    理解 Python 中的 ThreadLocal 变量 (一)

    原文链接地址 http://python.jobbole.com/

    多线程环境下,每一个线程均可以使用所属进程的全局变量。如果一个线程对全局变量进行了修改,将会影响到其他所有的线程。为了避免多个线程同时对变量进行修改,引入了线程同步机制,通过互斥锁、条件变量或读写锁来控制对全局变量的访问。

    只用全局变量并不能满足多线程环境的需求,很多时候线程还需要拥有自己的私有数据,这些数据对于其他线程来说不可见。因此线程中也可以使用局部变量,局部变量只有线程自身可以访问,同一个进程下的其他线程不可访问。

    使用局部变量不太方便,Python 还提供了 ThreadLocal 变量,它本身是一个全局变量,但是每个线程却可以利用它来保存属于自己的私有数据,这些私有数据对其他线程也是不可见的。

    1. 全局变量与局部变量

    多线程环境下全局变量的同步。

    #!/usr/bin/env python3 # -*- coding:utf-8 -*- import threading global_num = 0 def thread_cal(): global global_num for i in range(1000): global_num += 1 # Get 10 threads, run them and wait them all finished. threads = [] for i in range(10): threads.append(threading.Thread(target=thread_cal)) threads[i].start() for i in range(10): threads[i].join() # Value of global variable can be confused. print(global_num) /usr/bin/python3.5 /home/strong/workspace/master.py 10000 Process finished with exit code 0

    我们创建 10 个线程,每个线程均对全局变量 global_num 进行 1000 次的加 1 操作 (循环 1000 次加 1 是为了延长单个线程执行时间,使线程执行时被中断切换)。当 10 个线程执行完毕时,全局变量的值不确定。因为 global_num += 1 并不是一个原子操作,执行过程可能被其他线程中断,导致其他线程读到一个脏值。以两个线程执行 +1 为例,其中一个可能的执行序列如下。

    多线程中使用全局变量时普遍存在这个问题,解决办法也很简单,可以使用互斥锁、条件变量或是读写锁。下面考虑用互斥锁来解决上面代码的问题,只需要在进行 +1 运算前加锁,运算完毕释放锁即可,这样就可以保证运算的原子性。

    l = threading.Lock() ... l.acquire() global_num += 1 l.release()

    在线程中使用局部变量则不存在这个问题,每个线程的局部变量不能被其他线程访问。下面我们用 10 个线程分别对各自的局部变量进行 1000 次加 1 操作,每个线程结束时打印一共执行的操作次数 (每个线程均为 1000)。

    #!/usr/bin/env python3 # -*- coding:utf-8 -*- import threading def show(num): print(threading.current_thread().getName(), num) def thread_cal(): local_num = 0 for _ in range(1000): local_num += 1 show(local_num) # Get 10 threads, run them and wait them all finished. threads = [] for i in range(10): threads.append(threading.Thread(target=thread_cal)) threads[i].start() for i in range(10): threads[i].join() /usr/bin/python3.5 /home/strong/pyqt5_workspace/task_master.py Thread-1 1000 Thread-2 1000 Thread-3 1000 Thread-4 1000 Thread-5 1000 Thread-6 1000 Thread-7 1000 Thread-8 1000 Thread-9 1000 Thread-10 1000 Process finished with exit code 0

    可以看出这里每个线程都有自己的 local_num,各个线程之间互不干涉。

    2. Thread-local 对象

    建立一个全局字典,保存进程 ID 到该进程局部变量的映射关系,运行中的线程可以根据自己的 ID 来获取本身拥有的数据。可以避免在函数调用中传递参数。

    #!/usr/bin/env python3 # -*- coding:utf-8 -*- import threading global_data = {} def show(): cur_thread = threading.current_thread() print(cur_thread.getName(), global_data[cur_thread]) def thread_cal(): global global_data cur_thread = threading.current_thread() global_data[cur_thread] = 0 for _ in range(1000): global_data[cur_thread] += 1 show() # Need no local variable. # Get 10 threads, run them and wait them all finished. threads = [] for i in range(10): threads.append(threading.Thread(target=thread_cal)) threads[i].start() for i in range(10): threads[i].join() /usr/bin/python3.5 /home/strong/workspace/master.py Thread-1 1000 Thread-2 1000 Thread-3 1000 Thread-4 1000 Thread-5 1000 Thread-6 1000 Thread-7 1000 Thread-8 1000 Thread-9 1000 Thread-10 1000 Process finished with exit code 0

    保存一个全局字典,然后将线程标识符作为key,相应线程的局部数据作为 value,这种做法并不完美。首先,每个函数在需要线程局部数据时,都需要先取得自己的线程ID,略显繁琐。更糟糕的是,这里并没有真正做到线程之间数据的隔离,因为每个线程都可以读取到全局的字典,每个线程都可以对字典内容进行更改。

    为了更好解决这个问题,python 线程库实现了 ThreadLocal 变量 (Java 语言有类似的实现)。ThreadLocal 真正做到了线程之间的数据隔离,并且使用时不需要手动获取自己的线程 ID。

    #!/usr/bin/env python3 # -*- coding:utf-8 -*- import threading global_data = threading.local() def show(): print(threading.current_thread().getName(), global_data.num) def thread_cal(): global_data.num = 0 for _ in range(1000): global_data.num += 1 show() # Get 10 threads, run them and wait them all finished. threads = [] for i in range(10): threads.append(threading.Thread(target=thread_cal)) threads[i].start() for i in range(10): threads[i].join() print("Main thread: ", global_data) /usr/bin/python3.5 /home/strong/workspace/master.py Thread-1 1000 Thread-2 1000 Thread-3 1000 Thread-4 1000 Thread-5 1000 Thread-6 1000 Thread-7 1000 Thread-8 1000 Thread-9 1000 Thread-10 1000 Main thread: <_thread._local object at 0x7fa6939cdeb8> Process finished with exit code 0

    每个线程都可以通过 global_data.num 获得自己独有的数据,并且每个线程读取到的 global_data 都不同,真正做到线程之间的隔离。

    Python 的 WSGI 工具库 werkzeug 中有一个更好的 ThreadLocal 实现,甚至支持协程之间的私有数据,实现更加复杂。

    最新回复(0)