werkzeug

一、简介

在local模块中,Werkzeug实现了类似Python标准库中thread.local的功能。thread.local是线程局部变量,也就是每个线程的私有变量,具有线程隔离性,可以通过线程安全的方式获取或者改变线程中的变量。参照thread.local,Werkzeug实现了比thread.local更多的功能。Werkzeug官方文档关于local模块中对此进行了说明:

The Python standard library comes with a utility called “thread locals”. A thread local is a global object in which you can put stuff in and get back later in a thread-safe way. That means whenever you set or get an object on a thread local object, the thread local object checks in which thread you are and retrieves the correct value.

This, however, has a few disadvantages. For example, besides threads there are other ways to handle concurrency in Python. A very popular approach is greenlets. Also, whether every request gets its own thread is not guaranteed in WSGI. It could be that a request is reusing a thread from before, and hence data is left in the thread local object.

总结起来:以上文档解释了对于“并发”问题,多线程并不是唯一的方式,在Python中还有“协程”(关于协程的概念和用法可以参考:廖雪峰的博客)。“协程”的一个显著特点在于是一个线程执行,一个线程可以存在多个协程。也可以理解为:协程会复用线程。对于WSGI应用来说,如果每一个线程处理一个请求,那么thread.local完全可以处理,但是如果每一个协程处理一个请求,那么一个线程中就存在多个请求,用thread.local变量处理起来会造成多个请求间数据的相互干扰。

对于上面问题,Werkzeug库解决的办法是local模块。local模块实现了四个类:

Local

LocalStack

LocalProxy

LocalManager

本文重点介绍前两个类的实现。

二、Local类

Local类能够用来存储线程的私有变量。在功能上这个thread.local类似。与之不同的是,Local类支持Python的协程。在Werkzeug库的local模块中,Local类实现了一种数据结构,用来保存线程的私有变量,对于其具体形式,可以参考它的构造函数:

Python

classLocal(object):

__slots__=('__storage__','__ident_func__')

def__init__(self):

object.__setattr__(self,'__storage__',{})

object.__setattr__(self,'__ident_func__',get_ident)

从上面类定义可以看出,Local类具有两个属性:__storage__和__ident_func__。从构造函数来看,__storage__是一个字典,而__ident_func__是一个函数,用来识别当前线程或协程。

1.__ident_func__

关于当前线程或协程的识别,local模块引入get_ident函数。如果支持协程,则从greenlet库中导入相关函数,否则从thread库中导入相关函数。调用get_ident将返回一个整数,这个整数可以确定当前线程或者协程。

Python

try:

fromgreenletimportgetcurrentasget_ident

exceptImportError:

try:

fromthreadimportget_ident

exceptImportError:

from_threadimportget_ident

2.__storage__

__storage__是一个字典,用来存储不同的线程/协程,以及这些线程/协程中的变量。以下是一个简单的多线程的例子,用来说明__storage__的具体结构。

Python

importthreading

fromwerkzeug.localimportLocal

l=Local()

l.__storage__

defadd_arg(arg,i):

l.__setattr__(arg,i)

foriinrange(3):

arg='arg'+str(i)

t=threading.Thread(target=add_arg,args=(arg,i))

t.start()

l.__storage__

上面的例子,具体分析为:

首先,代码创建了一个Local的实例l,并且访问它的__storage__属性。由于目前还没有数据,所以l.__storage__的结果为{};

代码创建了3个线程,每个线程均运行add_arg(arg, i)函数。这个函数会为每个线程创建一个变量,并对其赋值;

最后,再次访问l.__storage__。这次,l实例中将包含3个线程的信息。其结果为:

Python

1

{20212:{'arg0':0},20404:{'arg1':1},21512:{'arg2':2}}

从以上结果可以看出,__storage__这个字典的键表示不同的线程(通过get_ident函数获得线程标识数值),而值表示对应线程中的变量。这种结构将不同的线程分离开来。当某个线程要访问该线程的变量时,便可以通过get_ident函数获得线程标识数值,进而可以在字典中获得该键对应的值信息了。

三、LocalStack类

LocalStack类和Local类类似,但是它实现了栈数据结构。

在LocalStack类初始化的时候,便会创建一个Local实例,这个实例用于存储线程/协程的变量。与此同时,LocalStack类还实现了push、pop、top等方法或属性。调用这些属性或者方法时,该类会根据当前线程或协程的标识数值,在Local实例中对相应的数值进行操作。以下还是以一个多线程的例子进行说明:

Python

fromwerkzeug.localimportLocalStack,LocalProxy

importlogging,random,threading,time

# 定义logging配置

logging.basicConfig(level=logging.DEBUG,

format='(%(threadName)-10s) %(message)s',

)

# 生成一个LocalStack实例_stack

_stack=LocalStack()

# 定义一个RequestConetxt类,它包含一个上下文环境。

# 当调用这个类的实例时,它会将这个上下文对象放入

# _stack栈中去。当退出该上下文环境时,栈会pop其中

# 的上下文对象。

classRequestConetxt(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):

ifexc_tbisNone:

_stack.pop()

def__repr__(self):

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

# 定义一个可供不同线程调用的方法。当不同线程调用该

# 方法时,首先会生成一个RequestConetxt实例,并在这

# 个上下文环境中先将该线程休眠一定时间,之后打印出

# 目前_stack中的信息,以及当前线程中的变量信息。

# 以上过程会循环两次。

defworker(i):

withrequest_context(i):

forjinrange(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对象

defrequest_context(i):

i=str(i+1)

returnRequestConetxt('a'+i,'b'+i,'c'+i)

# 在程序最开始显示_stack的最初状态

logging.debug('Stack Initial State: %s'%_stack._local.__storage__.items())

# 产生两个线程,分别调用worker函数

foriinrange(2):

t=threading.Thread(target=worker,args=(i,))

t.start()

main_thread=threading.currentThread()

fortinthreading.enumerate():

iftisnotmain_thread:

t.join()

# 在程序最后显示_stack的最终状态

logging.debug('Stack Finally State: %s'%_stack._local.__storage__.items())

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

首先,先创建一个LocalStack实例_stack,这个实例将存储线程/协程的变量信息;

在程序开始运行时,先检查_stack中包含的信息;

之后创建两个线程,分别执行worker函数;

worker函数首先会产生一个上下文对象,这个上下文对象会放入_stack中。在这个上下文环境中,程序执行一些操作,打印一些数据。当退出上下文环境时,_stack会pop该上下文对象。

在程序结束时,再次检查_stack中包含的信息。

运行上面的测试例子,产生结果如下:

Python

(MainThread)StackInitialState:[]

(Thread-1)Sleeping0.31

(Thread-2)Sleeping0.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)Sleeping0.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)Sleeping0.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)StackFinallyState:[]

注意到:

当两个线程在运行时,_stack中会存储这两个线程的信息,每个线程的信息都保存在类似{'stack': [a1, b1, c1]}的结构中(注:stack键对应的是放入该栈中的对象,此处为了方便打印了该对象的一些属性)。

当线程在休眠和运行中切换时,通过线程的标识数值进行区分不同线程,线程1运行时它通过标识数值只会对属于该线程的数值进行操作,而不会和线程2的数值混淆,这样便起到线程隔离的效果(而不是通过锁的方式)。

由于是在一个上下文环境中运行,当线程执行完毕时,_stack会将该线程存储的信息删除掉。在上面的运行结果中可以看出,当线程2运行结束后,_stack中只包含线程1的相关信息。当所有线程都运行结束,_stack的最终状态将为空。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容