threading.local

为什么用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中提供了两种上下文管理机制

一个是请求上下文(request context),另一个应用上下文(application context)

上下文流程

从源码中来说: 上下文管理流程分为三个阶段

我觉得可以把上下文管理分为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掉.
为了方便获取这两种上下文的信息,flask提供了四个变量

 

 激活上下文

当请求进入时,Flask会自动激活请求上下文,这时候我们可以使用request和session变量,另外程序上下文也会被激活,当请求处理完毕时,这两种上下文都会自动销毁,也就是他俩的生命周期是一样的.

如果我们在没有激活上下文时,使用这些变量,flask就会抛出异常Runingtimeerror ,working outside of application context.
同样依赖于上下文的还有url_for()函数,和jsonify()函数,只能 在视图函数中使用它.
手动激活上下文
如果你要在没有激活上下文的情况下使用这些变量可以手动激活上下文
程序上下文使用app.app_context()获取,我们可以使用with语句执行上下文操作
from app import app
from flask import current_app
with  app.app_context():
        print(current_app.name)

 在源码中这样体现

 

 或者是显示地使用push方法激活上下文,在执行完相关操作时使用pop()方法销毁上下文:

from  app import  app
from flask import current_app

app_ctx = app.app_context()
app.ctx.push()
print(current_app.name)

而请求上下文可以通过text_request_context()方法临时创建

from app import app
from flask import request

with app.test_request_context('/hello'):
    print(request.method)
 
这个在测试应用程序的时候用的比较多,详情参见http://www.bjhee.com/flask-ad9.html

上下文钩子

flask也为上下文提供了一个teardown_appcontext钩子,使用它注册的回调函数会在程序上下文被销毁或请求上下文被销毁时调用,比如你需要在每个请求处理结束后销毁数据库连接

@app.teardown_appcontext
def teardown_db(exception)
    db.close()

#使用该装饰器注册的回调函数需要接受异常对象作为参数,当请求被正常处理的时候,这个参数值将是None,这个函数的返回值将被忽略.

 

localstack、local、字典之间的关系?
local是使用了字典的方式实现线程隔离,localStack把local对象作为他自己的一个属性,来作为线程隔离的一个栈结构。
 

flask使用线程隔离的意义?

意义在于:使用当前线程能够正确引用到他自己所创建的对象,而不是引用到其他线程所创建的对象。这就是线程隔离的意义

体会:封装如果一次封装解决不了问题,那么就再来一次。
 
 
 

 

posted on 2018-04-27 15:49  程序员一学徒  阅读(305)  评论(0编辑  收藏  举报