一、上下文管理理论基础
1.线程数据隔离
多线程访问一个数据:当内存中存在一个数据,多个线程都可以对其进行修改,如果要保证数据的一致性,则需要对其进行加锁。
多线程操作自己的数据:当需要每个线程都只能操作自己的数据,而该数据需要放置到一个全局的空间(例如全局变量)。则需要对其进行数据隔离,即线程只能访问自己存储的数据。
在threading模块中,我们可以使用threading.local来实现线程之间的数据隔离:
import threading import time # 定义一个threading.local对象 obj = threading.local def run(index): # 写入xxx=index obj.xxx = index # 10个线程 for i in range(10): t = threading.Thread(target=run, agrs=(i,)) t.start()
虽然每个线程都对obj写入了一个名为"xxx"的变量,值为自己的index。但是threading.local对象为各个线程做了数据隔离。
他的原理是,为每一个线程都开辟一块内存空间,实际上就是利用一个字典,将线程的唯一表示作为键key,线程存入的值放到该键对应的value中。如下所示:
# threading.local使用一个字典来保存各个线程的数据 { 1233: {'xxx': 0}, # 第0个线程的tid为1233,存入的值放在对应的字典中 1234: {'xxx': 1}, 1235: {'xxx': 2}, 1236: {'xxx': 3}, 1237: {'xxx': 4}, 1238: {'xxx': 5}, 1239: {'xxx': 6}, 1240: {'xxx': 7}, 1241: {'xxx': 8}, 1242: {'xxx': 9}, }
2.用字典实现一个threading.local类
import threading class Local(object): DIC = {} # DIC字典用于存放各线程的数据,通过key来隔离 # 从DIC中线程tid对应的字典中获取值 def __getattr__(self, item): tid = threading.get_ident() if tid in self.DIC: return self.DIC[tid].get(item) else: return None # 设置一个值,类似obj.xxx = 1 def __setattr__(self, key, value): # 获取该线程的tid tid = threading.get_ident() # 如果DIC中存在键为tid的数据 if tid in self.DIC: self.DIC[tid][key] = value else: self.DIC[tid] = {key: value} # 创建一个Local对象 obj = Local() def run(index): # 使用Local对象保存各线程的数据 obj.xxx = index # 开启10个线程 for i in range(10): t = threading.Thread(target=run, args=(i,)) t.start() # 打印最后的值 print( obj.DIC) # {2852: {'xxx': 0}, 13520: {'xxx': 1}, 10756: {'xxx': 2}, 7488: {'xxx': 3}, 8484: {'xxx': 4}, 13924: {'xxx': 5}, 10668: {'xxx': 6}, 10252: {'xxx': 7}, 11348: {'xxx': 8}, 10736: {'xxx': 9}}
3.将Local支持协程
我们实现的Local类,达到了和threading.local一样的效果,可以隔离线程的数据。但是我们如果使用的是协程,则需要对其进行扩展。
import threading try: import greenlet # 使用协程时,将协程获取唯一标识的方法赋值给get_ident get_ident = greenlet.getcurrent print("使用协程") except Exception as e: print("使用线程") # 没有使用协程时,将线程获取唯一标识的方法赋值给get_ident get_ident = threading.get_ident class Local(object): DIC = {} # DIC字典用于存放各线程的数据,通过key来隔离 # 从DIC中线程tid对应的字典中获取值 def __getattr__(self, item): tid = get_ident() if tid in self.DIC: return self.DIC[tid].get(item) else: return None # 设置一个值,类似obj.xxx = 1 def __setattr__(self, key, value): # 获取该线程的tid tid = get_ident() # 如果DIC中存在键为tid的数据 if tid in self.DIC: self.DIC[tid][key] = value else: self.DIC[tid] = {key: value} # 创建一个Local对象 obj = Local() def run(index): # 使用Local对象保存各线程的数据 obj.xxx = index # 开启10个线程 for i in range(10): t = threading.Thread(target=run, args=(i,)) t.start() # 打印最后的值 print(obj.DIC)
要扩展支持协程,其实很简单,就是让唯一标识从线程的唯一标识替换为协程的唯一标识。
4.线程、协程数据隔离和Flask的关系
虽然threading.local和Flask没有直接的关系,但是Flask中实现了一套利用此原理的数据隔离机制。类似我们第3.节中实现的支持线程和协程的版本。
在Flask中,请求相关的数据和session等数据都是通过上下文管理的。我们可以将上下文看成一个全局的数据存放点。而我们需要对其数据进行隔离,因为Flask有可能底层会使用多线程、协程等方式来运行。
多个线程或协程会同时接受来自用户的请求,而如果不进行数据隔离,则可能数据会相互覆盖,从而导致数据错误。
##