为什么用threading.local?
我们都知道线程是由进程创建出来的,CPU实际执行的也是线程,那么线程其实是没有自己独有的内存空间的,所有的线程共享进程的资源和空间,共享就会有冲突,对于多线程对同一块数据处理的冲突问题,一个办法就是加互斥锁,另一个办法就是利用threading.local
threading.local 实现的的基本思路: 给一个进程中的多个线程开辟独立的空间来分别保存它们的值.这样它就不会更改全局变量了.
情况一,开多个线程更改全局变量,然后每次线程打印全局变量的值
from threading import Thread
import time
local_values =3
def f(args):
global local_values #更改全局变量
local_values=args
time.sleep(3) #如果这里不加等待时间,就不存在抢空间的说法
print(local_values)#结果就等于最后一个更改的值
for i in range(20):
thread_=Thread(target=f,args=(i,))
thread_.start()
结果:
19
19
19
19
19
19
19
19
19
19
19
19
19
19
19
19
19
19
19
19
View Code
这样就造成了数据不安全,本来我们设想的是1,2,3,4,5,6,7,8'...,最后怎么都变成19了,原因在于,线程之前数据是共享的,当你同时开多个线程时候,遇到io阻塞就会发生数据污染,也就是互斥现象.
怎么解决这个问题呢?
方法一:我们加互斥锁,让更改数据的那部分变成顺序执行,见互斥锁博客中的互斥锁关于线程
方法二:这个是我们要介绍的重点
就出现了local类
import threading
from threading import local
from threading import Thread
import time
#实例化一个local对象
local_values =local()
def f(args):
local_values.name=args#给每次进程中添加内容
time.sleep(1)
print(local_values.name,threading.current_thread().name)
for i in range(20):
thread_=Thread(target=f,args=(i,),name='线程%s'%(i))
thread_.start()
结果:
0 线程0
1 线程1
6 线程6
4 线程4
5 线程5
2 线程2
3 线程3
11 线程11
10 线程10
9 线程9
7 线程7
8 线程8
16 线程16
17 线程17
14 线程14
12 线程12
15 线程15
13 线程13
19 线程19
18 线程18
View Code
这样就解决了,依据这个思路,我们自己实现给线程开辟独有的空间保存特有的值
threading.local对于flask处理并发
那么Flask也可以用thread.local来处理大量请求啊,但是Flask并不没有使用threading.local,而是使用了类似threading.local的东西来处理的并发请求
我们知道线程和协程都有自己的get_ident 或getcurrent也就是唯一标识,我们可以用这个唯一标识当做键,以每个线程中保存的值为value,为线程开辟独有的空间来保存值
import threading
try:
from greenlet import getcurrent as get_ident # 没有携程的时候就用线程
except Exception as e:
from threading import get_ident
from threading import Thread
import time
# 创建一个类似于threading.Local的类
class Local_demo:
def __init__(self):
self.dict1 = {} # 创建一个空字典用来保存所有的线程要存储的值
def set(self, k, v):
"""
把每个线程要储存的值放到字典中
:param k:
:param v:
:return:
"""
ident = get_ident() # 获取每一个线程的唯一标识
if ident in self.dict1:
self.dict1[ident][k] = v
else:
self.dict1[ident] = {k: v}
def get(self, k):
"""
根据k值获取当前线程保留在字典中的值
:param k:
:return:
"""
ident = get_ident()
return self.dict1[ident][k]
obj = Local_demo()
def f(arg):
obj.set("k", arg)
time.sleep(3)
print("%s存储的值是%s"%( threading.current_thread().name,obj.get('k')))
if __name__ == '__main__':
#开启多线程
for i in range(20):
thread_ = Thread(target=f, args=(i,), name='线程%s' % (i))
thread_.start()
View Code
结果:
线程3存储的值是3
线程1存储的值是1
线程0存储的值是0
线程2存储的值是2
线程8存储的值是8
线程6存储的值是6
线程5存储的值是5
线程4存储的值是4
线程7存储的值是7
线程15存储的值是15
线程14存储的值是14
线程13存储的值是13
线程12存储的值是12
线程11存储的值是11
线程10存储的值是10
线程9存储的值是9
线程19存储的值是19
线程18存储的值是18
线程17存储的值是17
线程16存储的值是16
View Code
让我们来继续优化下,注意我们在f函数中用到了obj.set来调用set()函数,我们可以用对象.没有的属性来调用__setattr__和__getattr__,这样吧set和get函数名换成__setattr__ 和 __getattr__ 这样就显得有逼格多了
第三种代码:
import threading
try:
from greenlet import getcurrent as get_ident #没有携程的时候就用线程
except Exception as e:
from threading import get_ident
from threading import Thread
import time
# 实例化一个local对象
class Local_demo(object):
def __init__(self):
# self.dict1 = {} # 在这就不能这样创建字典了,因为有__setattr__,这样创建就会频繁调用该函数,变成递归无限了
object.__setattr__(self,'dict1',{})#这里一定要用object来调用
def __setattr__(self, k, v):
ident = get_ident()
if ident in self.dict1:
self.dict1[ident][k] = v
else:
self.dict1[ident] = {k: v}
def __getattr__(self, k):
ident = get_ident()
return self.dict1[ident][k]
obj = Local_demo()
def f(arg):
obj.val=arg
time.sleep(3)
print(obj.val, threading.current_thread().name)
for i in range(20):
thread_ = Thread(target=f, args=(i,), name='线程%s' % (i))
thread_.start()
优化代码
结果:
2 线程2
1 线程1
0 线程0
4 线程4
3 线程3
7 线程7
6 线程6
5 线程5
10 线程10
9 线程9
8 线程8
12 线程12
13 线程13
11 线程11
16 线程16
15 线程15
14 线程14
18 线程18
17 线程17
19 线程19
View Code
我们再说回Flask,我们知道django中的request是直接当参数传给视图的,这就不存在几个视图修改同一个request的问题,但是flask不一样,flask中的request是导入进去的,就相当于全局变量,每一个视图都可以对它进行访问修改取值操作,这就带来了共享数据的冲突问题,解决的思路就是我们上边的第三种代码,利用协程和线程的唯一标识作为key,也是存到一个字典里,类中也是采用__setattr__和__getattr__方法来赋值和取值.
Flask中有个local对象建立方式和第三种代码的建立方式一样,利用协程和线程的唯一标识作为key,具体的形式为
{
线程或协程唯一标识: { 'stack':[request],'xxx':[session,] },
线程或协程唯一标识: { 'stack':[] },
线程或协程唯一标识: { 'stack':[] },
线程或协程唯一标识: { 'stack':[] },
}
上下文管理机制
在Flask中提供了两种上下文管理机制
一个是请求上下文,另一个应用上下文
从源码中来说: 上下文管理流程分为三个阶段
我觉得可以把上下文管理分为3个阶段
第一个阶段是,请求进来后把请求相关数据封装到local对象中,
第二个阶段是:在视图函数中,视图函数从local对象中取出请求相关的数据
第三个阶段:请求结束后,把local对象中的相关数据进行销毁,
如果说的更细一点:
请求进来之后:
首先对封装了两个对象,其中一个叫RequestContext,里边封装了request和session,另一个叫appcontext,里边封装了app和g,,接下来基于LocalStack栈把这两个对象分别放到两个local的对象中存放起来,这个local对象类似于threading.local,但是他的粒度更细一下,基于协程来做的 用来保存和隔离每次的请求数据
视图阶段:
是基于LocalProxy类,localProxy 调用偏函数partial,通多partial再调用LOcalStack这样就可以把存在Local对象中的数据取出来.
请求结束:
先把local中的session写到cookie中,然后再 调用LocalStack.pop把local中的数据pop掉