理解 Python 中的 ThreadLocal 变量 (一)

理解 Python 中的 ThreadLocal 变量 (一)

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

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

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

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

理解 Python 中的 ThreadLocal 变量 (一)_第1张图片

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 为例,其中一个可能的执行序列如下。

理解 Python 中的 ThreadLocal 变量 (一)_第2张图片

多线程中使用全局变量时普遍存在这个问题,解决办法也很简单,可以使用互斥锁、条件变量或是读写锁。下面考虑用互斥锁来解决上面代码的问题,只需要在进行 +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 实现,甚至支持协程之间的私有数据,实现更加复杂。

你可能感兴趣的:(process,(进程),and,thread,(线程),理解,Python,中的,ThreadLocal,变量,(一))