原文链接地址 http://python.jobbole.com/
多线程环境下,每一个线程均可以使用所属进程的全局变量。如果一个线程对全局变量进行了修改,将会影响到其他所有的线程。为了避免多个线程同时对变量进行修改,引入了线程同步机制,通过互斥锁、条件变量或读写锁来控制对全局变量的访问。
只用全局变量并不能满足多线程环境的需求,很多时候线程还需要拥有自己的私有数据,这些数据对于其他线程来说不可见。因此线程中也可以使用局部变量,局部变量只有线程自身可以访问,同一个进程下的其他线程不可访问。
使用局部变量不太方便,Python 还提供了 ThreadLocal 变量,它本身是一个全局变量,但是每个线程却可以利用它来保存属于自己的私有数据,这些私有数据对其他线程也是不可见的。
多线程环境下全局变量的同步。
#!/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,各个线程之间互不干涉。
建立一个全局字典,保存进程 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 实现,甚至支持协程之间的私有数据,实现更加复杂。