咱们每个请求进来的时候都开一个进程确定不合理,那么若是每个请求进来都是串行的,那么根本实现不了并发,因此咱们假定每个请求进来使用的是线程。python
那么线程中数据互相不隔离,存在修改数据的时候数据不安全的问题。flask
假定咱们的需求是,每一个线程都要设置值,而且该线程打印该线程修改的值。安全
from threading import Thread,current_thread import time class Foo(object): def __init__(self): self.name = 0 locals_values = Foo() def func(num): locals_values.name = num time.sleep(2) # 取出该线程的名字 print(locals_values.name, current_thread().name) for i in range(10): # 设置该线程的名字 t = Thread(target=func,args=(i,),name='线程%s'%i) t.start()
很明显阻塞了2秒的时间全部的线程都完成了修改值,而2秒后全部的线程打印出来的时候都是9了,就产生了数据不安全的问题。多线程
因此咱们要解决这种线程不安全的问题,有以下两种解决方案。并发
方案一:是加锁框架
方案二:使用threading.local
对象把要修改的数据复制一份,使得每一个数据互不影响。ide
咱们要实现的并发是多个请求实现并发,而不是纯粹的只是修改一个数据,因此第二种思路更适合作咱们每一个请求的并发,把每一个请求对象的内容都复制一份让其互相不影响。性能
详解:为何不用加锁的思路?加锁的思路是多个线程要真正实现共用一个数据,而且该线程修改了数据以后会影响到其余线程,更适合相似于12306抢票的应用场景,而咱们是要作请求对象的并发,想要实现的是该线程对于请求对象这部份内容有任何修改并不影响其余线程。因此使用方案二测试
多个线程修改同一个数据,复制多份数据给每一个线程用,为每一个线程开辟一块空间进行数据存储操作系统
实例:
from threading import Thread,current_thread,local import time locals_values = local() # 能够简单理解为,识别到新的线程的时候,都会开辟一片新的内存空间,至关于每一个线程对该值进行了拷贝。 def func(num): locals_values.name = num time.sleep(2) print(locals_values.name, current_thread().name) for i in range(10): t = Thread(target=func,args=(i,),name='线程%s'%i) t.start()
如上经过threading.local实例化的对象,实现了多线程修改同一个数据,每一个线程都复制了一份数据,而且修改的也都是本身的数据。达到了咱们想要的效果。
实例:
from threading import get_ident,Thread,current_thread # get_ident()能够获取每一个线程的惟一标记, import time class Local(object): storage = {}# 初始化一个字典 get_ident = get_ident # 拿到get_ident的地址 def set(self,k,v): ident =self.get_ident()# 获取当前线程的惟一标记 origin = self.storage.get(ident) if not origin: origin={} origin[k] = v self.storage[ident] = origin def get(self,k): ident = self.get_ident() # 获取当前线程的惟一标记 v= self.storage[ident].get(k) return v locals_values = Local() def func(num): # get_ident() 获取当前线程的惟一标记 locals_values.set('KEY',num) time.sleep(2) print(locals_values.get('KEY'),current_thread().name) for i in range(10): t = Thread(target=func,args=(i,),name='线程%s'%i) t.start()
讲解:
利用get_ident()
获取每一个线程的惟一标记做为键,而后组织一个字典storage。
如:{线程1的惟一标记:{k:v},线程2的惟一标记:{k:v}.......}
{ 15088: {'KEY': 0}, 8856: {'KEY': 1}, 17052: {'KEY': 2}, 8836: {'KEY': 3}, 13832: {'KEY': 4}, 15504: {'KEY': 5}, 16588: {'KEY': 6}, 5164: {'KEY': 7}, 560: {'KEY': 8}, 1812: {'KEY': 9} }
运行效果:
实例:
from threading import get_ident,Thread,current_thread # get_ident()能够获取每一个线程的惟一标记, import time class Local(object): storage = {}# 初始化一个字典 get_ident = get_ident # 拿到get_ident的地址 def __setattr__(self, k, v): ident =self.get_ident()# 获取当前线程的惟一标记 origin = self.storage.get(ident) if not origin: origin={} origin[k] = v self.storage[ident] = origin def __getattr__(self, k): ident = self.get_ident() # 获取当前线程的惟一标记 v= self.storage[ident].get(k) return v locals_values = Local() def func(num): # get_ident() 获取当前线程的惟一标记 locals_values.KEY=num time.sleep(2) print(locals_values.KEY,current_thread().name) for i in range(10): t = Thread(target=func,args=(i,),name='线程%s'%i) t.start()
咱们能够自定义实现了threading.local的功能,可是如今存在一个问题,若是咱们想生成多个Local对象,可是会致使多个Local对象所管理的线程设置的内容都放到了类属性storage = {}里面,因此咱们若是想实现每个Local对象所对应的线程设置的内容都放到本身的storage里面,就须要从新设计代码。
实例:
from threading import get_ident,Thread,current_thread # get_ident()能够获取每一个线程的惟一标记, import time class Local(object): def __init__(self): # 千万不要按照注释里这么写,不然会形成递归死循环,死循环在__getattr__中,不理解的话能够全程使用debug测试。 # self.storage = {} # self.get_ident =get_ident object.__setattr__(self,"storage",{}) object.__setattr__(self,"get_ident",get_ident) # 借用父类设置对象的属性,避免递归死循环。 def __setattr__(self, k, v): ident =self.get_ident() # 获取当前线程的惟一标记 origin = self.storage.get(ident) if not origin: origin={} origin[k] = v self.storage[ident] = origin def __getattr__(self, k): ident = self.get_ident() # 获取当前线程的惟一标记 v= self.storage[ident].get(k) return v locals_values = Local() locals_values2 = Local() def func(num): # get_ident() 获取当前线程的惟一标记 # locals_values.set('KEY',num) locals_values.KEY=num time.sleep(2) print(locals_values.KEY,current_thread().name) # print('locals_values2.storage:',locals_values2.storage) # 查看locals_values2.storage的私有的storage for i in range(10): t = Thread(target=func,args=(i,),name='线程%s'%i) t.start()
显示效果我就不作演示了,和前几个案例演示效果同样。
状况一:单进程单线程,基于全局变量就能够作
状况二:单进程多线程,基于threading.local对象作
状况三:单进程多线程多协程,如何作?
提示:协程属于应用级别的,协程会替代操做系统自动切换遇到 IO
的任务或者运行级别低的任务,而应用级别的切换速度远高于操做系统的切换
固然若是是本身来设计框架,为了提高程序的并发性能,必定是上诉的状况三,不光考虑多线程而且要多协程,那么该如何设计呢?
在咱们的flask中为了这种并发需求,依赖于底层的werkzeug
外部包,werkzeug
实现了保证多线程和多协程的安全,werkzeug
基本的设计理念和上一个案例一致,惟一的区别就是在导入的时候作了一步处理,且看werkzeug
源码。
werkzeug.local.py
部分源码
... try: from greenlet import getcurrent as get_ident # 拿到携程的惟一标识 except ImportError: try: from thread import get_ident #线程的惟一标识 except ImportError: from _thread import get_ident class Local(object): ... def __init__(self): object.__setattr__(self, '__storage__', {}) object.__setattr__(self, '__ident_func__', get_ident) ... def __getattr__(self, name): try: return self.__storage__[self.__ident_func__()][name] except KeyError: raise AttributeError(name) def __setattr__(self, name, value): ident = self.__ident_func__() storage = self.__storage__ try: storage[ident][name] = value except KeyError: storage[ident] = {name: value}
分析:
原理就是在最开始导入线程和协程的惟一标识的时候统一命名为get_ident
,而且先导入协程模块的时候若是报错说明不支持协程,就会去导入线程的get_ident
,这样不管是只有线程运行仍是协程运行均可以获取惟一标识,而且把这个标识的线程或协程须要设置的内容都分类存放于__storage__
字典中。