当前位置: 首页 > 科技观察

深入理解Python中的ThreadLocal变量(下)

时间:2023-03-20 19:02:02 科技观察

上一篇我们看到了ThreadLocal变量的简单使用,第二篇分析了ThreadLocal在Python中的实现,但是故事还没有结束。这篇文章,我们来看看Werkzeug中ThreadLocal的设计。作为一个WSGI工具库,Werkzeug出于一些考虑并没有直接使用Python内置的ThreadLocal类,而是自己实现了一系列的Local类。包括简单的Local,以及在此基础上实现的LocalStack、LocalManager和LocalProxy。接下来,我们就来看看这些类的用法,设计的初衷,以及具体的实现技巧。Local类的设计Werkzeug的设计者认为Python自带的ThreadLocal不能满足需求,主要有以下两个原因:Werkzeug主要使用“ThreadLocal”来满足并发需求,而Python自带的ThreadLocal只能实现基于线程的并发。python中还有很多其他的并发方式,比如常见的协程(greenlets),所以需要实现一个可以支持协程的Local对象。WSGI不保证每次都会产生一个新的线程来处理请求,也就是说线程可以被复用(可以维护一个线程池来处理请求)。这样,如果werkzeug使用了Python自带的ThreadLocal,就会使用一个“不干净的(之前处理过的请求的相关数据)”线程来处理新的请求。为了解决这两个问题,在werkzeug中实现了Local类。Local对象可以隔离线程和协程之间的数据。此外,它还支持在线程或协程下清理数据(这样在处理完一个请求后,可以清理对应的数据,然后等待下一个请求的到来)。具体如何实现,思路其实很简单,我们在深入理解Python中线程局部变量(上)一文***中有提到,就是创建一个全局字典,然后使用线程(或协程)标识符作为键,相应线程(或协程)的本地数据作为值。这里的werkzeug就是按照上面的思路实现的,但是使用了python的一些黑魔法,最终为用户提供了一个清晰简洁的界面。Local类的具体实现在werkzeug.local中,使用8a84b62版本的代码进行分析。通过前两篇对ThreadLocal的了解,我们已经知道了Local对象的特点和用法。所以这里不再举例使用Local对象,我们直接看代码。classLocal(object):__slots__=('__storage__','__ident_func__')def__init__(self):object.__setattr__(self,'__storage__',{})object.__setattr__(self,'__ident_func__',get_ident)...由于to可能会有大量的Local对象。为了节省Local对象占用的空间,这里使用了__slots__来硬编码Local可以拥有的属性:__storage__:值为字典,用于保存实际数据,初始化为空;__ident_func__:值为一个函数,用于查找当前线程或协程的标识符。由于Local对象的实际数据存储在__storage__中,所以对Local属性的操作实际上就是对__storage__的操作。对于属性的获取,这里使用魔术方法__getattr__来拦截除__storage__和__ident_func__之外的属性的获取,并指向当前线程或协程存储在__storage__中的数据。对于属性值的set或del,分别使用__setattr__和__setattr__来实现(这些魔术方法的介绍见属性控制)。关键代码如下所示:def__getattr__(self,name):try:returnself.__storage__[self.__ident_func__()][name]exceptKeyError:raiseAttributeError(name)def__setattr__(self,name,value):ident=self.__ident_func__()存储=self.__storage__try:storage[ident][name]=valueexceptKeyError:storage[ident]={name:value}def__delattr__(self,name):try:delself.__storage__[self.__ident_func__()][name]exceptKeyError:raiseAttributeError(name)假设我们有N个线程或者协程,ID分别为1,2,...,N,每个线程或者协程都使用一个Local对象来保存自己的一些本地数据,那么Local对象的内容就是如下图所示:另外,Local类还提供了__release_local__方法来释放当前线程或协程保存的数据。Local扩展接口Werkzeug在Local的基础上实现了LocalStack和LocalManager,提供更友好的接口支持。LocalStackLocalStack通过对Local的封装,实现了一个线程(或协程)独立的栈结构。评论里有具体的使用方法。一个简单的用法示例如下:ls=LocalStack()ls.push(12)printls.top#12printls._local.__storage__#{140735190843392:{'stack':[12]}}LocalStack的实现比较有意思。它以一个Local对象作为自己的属性_local,然后定义接口push、pop、top方法来进行相应的入栈操作。这里使用_local.__storage__._local.__ident_func__()的列表来模拟栈结构。在接口push、pop和top中,通过操作list来模拟栈的操作。需要注意的是,在接口函数内部获取列表时,不需要像上面黑体字那么复杂。可以直接使用_local的getattr()方法。能。以push函数为例,实现如下:defpush(self,obj):"""Pushesanewitemtothestack"""rv=getattr(self._local,'stack',None)ifrvisNone:self._local.stack=rv=[]rv.append(obj)returnrvpop和top的实现和一般的栈类似,都是对stack=getattr(self._local,'stack',None)的链表进行相应的操作。此外,LocalStack还允许我们自定义__ident_func__。这里使用内置函数属性生成描述符,封装了__ident_func__的get和set操作,并提供了一个属性值__ident_func__作为接口。具体代码如下:都是线程或者协程独立的单个对象,很多时候我们需要一个线程或者协程无关的容器来组织多个Local或者LocalStack对象(就像我们用一个列表来组织多个int或者string类型一样)。Werkzeug实现了LocalManager,它通过一个列表类型的属性locals来存储托管的Local或LocalStack对象,同时也提供了一个清理方法来释放所有的Local对象。Werkzeug中LocalManager的主要接口是装饰器方法make_middleware,代码如下:defmake_middleware(self,app):"""WrapaWSGIapplicationsothatcleaninguphappensafterrequestend."""deapplication(environ,start_response):returnClosingIterator(app(environ,start_response),self.cleanup)returnapplication是注册回调函数清理的装饰器。当一个线程(或协程)处理完请求后,它会调用cleanup来清理它管理的Local或LocalStack对象(ClosingIterator的实现在werkzeug.wsgi中)。这是一个使用LocalManager的简单示例:fromwerkzeug.localimportLocal,LocalManagerlocal=Local()local_2=Local()local_manager=LocalManager([local,local2])defapplication(environ,start_response):local.request=request=Request(environ)...#application处理完后会自动清理local_manager的内容application=local_manager.make_middleware(application)通过LocalManager的make_middleware,我们可以在一个线程(协程)处理完一个请求后,清除所有的Local或者LocalStack对象,所以该线程可以处理另一个请求。至此,文章开头提到的第二个问题就可以解决了。Werkzeug.local还实现了一个LocalProxy作为Local对象的代理,同样值得学习。通过这三篇文章,相信我对ThreadLocal有了初步的了解。Python标准库和Werkzeug都在实现上大量使用了Python的黑魔法,但最终还是为用户提供了非常友好的界面。Werkzeug作为一个WSGI工具集,为解决Web开发中特定的使用问题,提供了一个改进版本,为了方便使用,做了一系列的封装。不得不说werkzeug的代码可读性很强,注释也写的很好。建议阅读源码。