Werkzeug库——local模块

local简介

  • local模块中,Werkzeug实现了类似Python标准库中thread.local的功能。
  • thread.local是线程局部变量,也就是每个线程的私有变量,具有线程隔离性,可以通过线程安全的方式获取或者改变线程中的变量。
  • 为什么不用python的内部库thread.local?
    • 在WSGI中会使用协程 , 而协程会复用线程。
    • 所以不能保证每个请求都有自己的线程,可能是请求正在重用以前的线程。
    • 一个线程中存在多个请求,用thread.local变量处理起来会造成多个请求间数据的相互干扰。

local模块实现了四个类

  • Local
  • LocalStack
  • LocalProxy
  • LocalManager

Local类

  • Local类能够用来存储线程/协程的私有变量。在功能上这个thread.local类似。

  • 与之不同的是,Local类支持Python的协程。在Werkzeug库的local模块中,Local类实现了一种数据结构,用来保存线程的私有变量

  • 对于其具体形式,可以参考它的构造函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    class Local(object):
    __slots__ = ('__storage__', '__ident_func__')
    def __init__(self):
    object.__setattr__(self, '__storage__', {})
    object.__setattr__(self, '__ident_func__', get_ident)

    # 迭代Local的时候 , 返回的是{20212: {'arg0': 0 , 'arg1': 1},}这样的字典格式
    def __iter__(self):
    return iter(self.__storage__.items())

    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}

    def __delattr__(self, name):
    try:
    del self.__storage__[self.__ident_func__()][name]
    except KeyError:
    raise AttributeError(name)
  • Local类具有两个属性:

    • __storage__ : 一个字典,用来存储不同的线程/协程 , 以及这些线程/协程中的变量。

      其数据结构为 :

      1
      2
      3
      4
      5
      {
      20212: {'arg0': 0 , 'arg1': 1},
      20404: {'arg1': 1},
      21512: {'arg2': 2}
      }
    • __ident_func__ : 一个函数,用来识别当前线程或协程。

__ident_func__

  • 用于识别当前线程或协程,
  • local模块引入get_ident函数。
    • 如果支持协程,则从greenlet库中导入相关函数,
    • 否则从thread库中导入相关函数。
  • 调用get_ident将返回一个整数,这个整数可以确定当前线程或者协程。
1
2
3
4
5
6
7
8
# # 在有greenlet的情况下,get_indent实际获取的是greenlet的id,而没有greenlet的情况下获取的是thread id
try:
from greenlet import getcurrent as get_ident
except ImportError:
try:
from thread import get_ident
except ImportError:
from _thread import get_ident

__storage__

  • __storage__是一个字典,用来存储不同的线程/协程,以及这些线程/协程中的变量。
  • 以下是一个简单的多线程的例子,用来说明__storage__的具体结构。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import threading
from werkzeug.local import Local

l = Local()
print(l.__storage__) # {}

def add_arg(arg, i):
l.__setattr__(arg, i)

for i in range(3):
arg = 'arg' + str(i)
t = threading.Thread(target=add_arg, args=(arg, i))
t.start()

print(l.__storage__) # {20212: {'arg0': 0}, 20404: {'arg1': 1}, 21512: {'arg2': 2}}
  • __storage__这个字典的键表示不同的线程(通过get_ident函数获得线程标识数值),而值表示对应线程中的变量。
  • 这种结构将不同的线程分离开来。当某个线程要访问该线程的变量时,便可以通过get_ident函数获得线程标识数值,进而可以在字典中获得该键对应的值信息了。

LocalStack类

  • LocalStack类和Local类类似,区别在于其数据结构是栈,而Local是字典的形式。

  • Local对象存储的时候是类似字典的方式,需要有key和value,而LocalStack是基于栈的,通过push和pop来存储和弹出数据。

  • 另外,当我们想释放存储空间的时候,也可以调用release_local()。

  • 请求上下文应用上下文的实现都是基于LocalStack。

LocalStack的_local属性是一个Local对象。我们的属性的键被定为stack,值是一个列表。通过这个列表实现了栈的push和pop。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class LocalStack(object):
def __init__(self):
self._local = Local()

def push(self, obj):
rv = getattr(self._local, "stack", None)
if rv is None:
self._local.stack = rv = []
rv.append(obj)
return rv

def pop(self):
stack = getattr(self._local, "stack", None)
if stack is None:
return None
elif len(stack) == 1:
release_local(self._local)
return stack[-1]
else:
return stack.pop()

@property
def top(self):
try:
return self._local.stack[-1]
except (AttributeError, IndexError):
return None
  • LocalStack类初始化的时候,便会创建一个Local实例,这个实例用于存储线程/协程的变量。
  • 与此同时,LocalStack类还实现了pushpoptop等方法或属性。调用这些属性或者方法时,该类会根据当前线程或协程的标识数值,在Local实例中对相应的数值进行操作。
  • 以下还是以一个多线程的例子进行说明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from werkzeug.local import LocalStack, LocalProxy
import logging, random, threading, time

# 定义logging配置
logging.basicConfig(level=logging.DEBUG,
format='(%(threadName)-10s) %(message)s',
)

# 生成一个LocalStack实例_stack
_stack = LocalStack()

# 定义一个RequestConetxt类,它包含一个上下文环境。
# 当调用这个类的实例时,它会将这个上下文对象放入
# _stack栈中去。当退出该上下文环境时,栈会pop其中
# 的上下文对象。
class RequestConetxt(object):
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c

def __enter__(self):
_stack.push(self)

def __exit__(self, exc_type, exc_val, exc_tb):
if exc_tb is None:
_stack.pop()

def __repr__(self):
return '%s, %s, %s' % (self.a, self.b, self.c)


# 定义一个可供不同线程调用的方法。当不同线程调用该
# 方法时,首先会生成一个RequestConetxt实例,并在这
# 个上下文环境中先将该线程休眠一定时间,之后打印出
# 目前_stack中的信息,以及当前线程中的变量信息。
# 以上过程会循环两次。
def worker(i):
with request_context(i):
for j in range(2):
pause = random.random()
logging.debug('Sleeping %0.02f', pause)
time.sleep(pause)
logging.debug('stack: %s' % _stack._local.__storage__.items())
logging.debug('ident_func(): %d' % _stack.__ident_func__())
logging.debug('a=%s; b=%s; c=%s' %
(LocalProxy(lambda: _stack.top.a),
LocalProxy(lambda: _stack.top.b),
LocalProxy(lambda: _stack.top.c)))
logging.debug('Done')

# 调用该函数生成一个RequestConetxt对象
def request_context(i):
i = str(i+1)
return RequestConetxt('a'+i, 'b'+i, 'c'+i)

# 在程序最开始显示_stack的最初状态
logging.debug('Stack Initial State: %s' % _stack._local.__storage__.items())

# 产生两个线程,分别调用worker函数
for i in range(2):
t = threading.Thread(target=worker, args=(i,))
t.start()

main_thread = threading.currentThread()
for t in threading.enumerate():
if t is not main_thread:
t.join()

# 在程序最后显示_stack的最终状态
logging.debug('Stack Finally State: %s' % _stack._local.__storage__.items())

以上例子的具体分析过程如下:

  • 首先,先创建一个LocalStack实例_stack,这个实例将存储线程/协程的变量信息
  • 在程序开始运行时,先检查_stack中包含的信息;
  • 之后创建两个线程,分别执行worker函数;
  • worker函数首先会产生一个上下文对象,这个上下文对象会放入_stack中。在这个上下文环境中,程序执行一些操作,打印一些数据。当退出上下文环境时,_stack会pop该上下文对象。
  • 在程序结束时,再次检查_stack中包含的信息。

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(MainThread) Stack Initial State: []
(Thread-1 ) Sleeping 0.31
(Thread-2 ) Sleeping 0.02
(Thread-2 ) stack: [(880, {'stack': [a1, b1, c1]}), (13232, {'stack': [a2, b2, c2]})]
(Thread-2 ) ident_func(): 13232
(Thread-2 ) a=a2; b=b2; c=c2
(Thread-2 ) Sleeping 0.49
(Thread-1 ) stack: [(880, {'stack': [a1, b1, c1]}), (13232, {'stack': [a2, b2, c2]})]
(Thread-1 ) ident_func(): 880
(Thread-1 ) a=a1; b=b1; c=c1
(Thread-1 ) Sleeping 0.27
(Thread-2 ) stack: [(880, {'stack': [a1, b1, c1]}), (13232, {'stack': [a2, b2, c2]})]
(Thread-2 ) ident_func(): 13232
(Thread-2 ) a=a2; b=b2; c=c2
(Thread-2 ) Done
(Thread-1 ) stack: [(880, {'stack': [a1, b1, c1]})]
(Thread-1 ) ident_func(): 880
(Thread-1 ) a=a1; b=b1; c=c1
(Thread-1 ) Done
(MainThread) Stack Finally State: []

注意到:

  • 当两个线程在运行时,_stack中会存储这两个线程的信息,每个线程的信息都保存在类似{'stack': [a1, b1, c1]}的结构中(注:stack键对应的是放入该栈中的对象,此处为了方便打印了该对象的一些属性)。
  • 当线程在休眠和运行中切换时,通过线程的标识数值进行区分不同线程,线程1运行时它通过标识数值只会对属于该线程的数值进行操作,而不会和线程2的数值混淆,这样便起到线程隔离的效果(而不是通过锁的方式)。
  • 由于是在一个上下文环境中运行,当线程执行完毕时,_stack会将该线程存储的信息删除掉。在上面的运行结果中可以看出,当线程2运行结束后,_stack中只包含线程1的相关信息。当所有线程都运行结束,_stack的最终状态将为空。

Local类和LocalStack类总结:

1
2
3
4
5
6
7
8
9
10
11
12
13
local = Local()
localstack = LocalStack()

# 下面本质是设置local的__storage__属性
# 而设置__storage__属性本质是设置__storage__[ident] = {name: value}
local.age = 21

# 下面本质是设置localstack的_local属性
localstack.school = 'gdgydx'

# 所以,当我们设置localstack.school = 'gdgydx' ,
# 其实就是设置 localstack._local.__storage__属性
# 进而,就是设置{<greenlet.greenlet at 0x4bee5a0>: {'school': 'gdgydx'},}