在深入理解Python中的ThreadLocal变量(上)中,我们看到了ThreadLocal的介绍,使得在多线程环境下使用局部变量变得简单。如此奇妙的功能是如何实现的呢?如果你没有好奇心,也没有想一探究竟其实现原理的冲动,那么下面的内容恐怕会让你后悔自己的小小品味。简单来说,Python中的ThreadLocal就是通过下图的方法,将全局变量伪装成线程局部变量。相信看完本文你就会明白图中的内容了。(如果你对这张图不熟悉,可以回顾一下之前的文章))。我在哪里可以找到源代码?好了,最后,我要分析一下ThreadLocal是如何实现的,但是等等,我如何找到它的源代码呢?在上一篇文章中,我们只是使用了它(fromthreadingimportlocal),从这里我们只能看出它是在threading模块中实现的,那么如何找到threading模块的源码呢。如果你使用的是PyCharm,恭喜你,你可以使用Viewsource(OSX快捷键是?↓)找到local的定义位置。现在很多IDE都有这个功能,你可以查看IDE的帮助找到这个功能。然后我们会发现local是这个样子的(这里以python2.7为例):我们要做的就是继续向下挖掘具体的实现,使用同样的方法(?↓)找到_local的实现。好像不是很好,也没有纯python实现:class_local(object):"""Thread-localdata"""def__delattr__(self,name):#realsignatureunknown;restoredfrom__doc__"""x.__delattr__('name')<==>delx.name"""pass...没关系,我们继续看_threading_local,现在终于找到了local的纯python实现。开头是一个很长的注释文档,告诉我们这个模块是什么以及如何使用它。这篇文档质量很高,值得学习。所以,再一次后悔自己的小品,差点错过这么优秀的文档模型!在我们私有化源码分析这个模块之前,我们先把它拷贝出来,放在一个单独的文件thread_local.py中,这样可以方便我们随意肢解它(比如在合适的地方加个log)place),并使用修改后的实现来验证我们的一些想法。另外,如果你真正理解了_threading_local.py的第一段,你就会发现这是多么的必要。因为python的threading.local不一定是_threading_local(还记得class_local(object)吗?)。所以如果你用threading.local来验证你对_threading_local.py的理解,你很可能会一头雾水。不幸的是,我开始这样做了,所以我被下面的代码卡住了很长一段时间:fromthreadingimportlocal,current_threaddata=local()key=object.__getattribute__(data,'_local__key')printcurrent_thread().__dict__.get(key)#AttributeError:'thread._local'objecthasnoattribute'_local__key'当然,你可能不明白这里是什么意思,没关系,我只是想强调一下,threading.local中并没有使用_threading_local.py,你必须创建一个模块(我称之为thread_local.py)来保存_threading_local的内容,然后验证你的想法如下:.__dict__.get(key)如何看懂源码现在你可以静下心来阅读这不到两百行的代码,但是等等,里面似乎有很多奇怪的内容(黑魔法):__slots____new____getattribute__/__setattr__/__delattr__Rlock什么这些是?如果你不知道,没关系,不要被这些纸老虎吓到,我们有丰富的文档,查文档搜索即可)。以下是我对上述内容的总结。如果你觉得自己看的清楚了,可以继续分析源码。如果还有不明白的地方,多看几遍文档(或者我说的不对,欢迎指出)。简单的说,在python中创建new-style类的实例时,会先调用__new__(cls[,...])创建实例,如果成功返回cls类型的对象,再调用__init__对对象进行初始化。在新的类中,我们可以使用__slots__来指定类可以拥有的属性名,这样每个对象就不会再创建__dict__,从而节省了对象占用的空间。特别是,基类的__slots__不会阻止派生类中__dict__的创建。可以通过重载__setattr__、__delattr__和__getattribute__方法来控制自定义类的属性访问(x.name),分别对应属性的赋值、删除和读取。锁是操作系统为了保证操作的原子性而引入的概念。python中的RLock是可重入锁(reentrantlock,又称递归锁),Rlock.acquire()可以多次进入而不会被阻塞。同一个线程。__dict__用于保存对象的(可写)属性,可以是字典或其他映射对象。源码分析对这些相关知识有了一个大概的了解之后,阅读源码就亲切多了。为了深入理解,我们先回忆一下本地对象是如何使用的,然后分析源码背后的调用过程。这里我们从定义一个最简单的线程局部对象开始,也就是说,当我们写下面这句话的时候发生了什么?data=local()上面这句话会调用_localbase.__new__为数据对象设置一些属性(有些属性我还不知道是干什么的,别怕,后面会讲),然后使用data的属性字典(__dict__)作为当前线程的一个属性值(该属性的key是根据id(data)生成的标识码)。这里值得深思:在创建ThreadLocal对象时,同时在线程的属性字典__dict__中保存了ThreadLocal对象的属性字典(也是对象,没错,万物皆对象)。还记得文章开头的那张图吗,红色虚线就是这个操作。然后我们考虑线程Thread-1中对ThreadLocal变量的一些常见操作,比如下面的操作序列:)#{}那么它在幕后是如何运作的?以上操作包括给属性赋值、读取属性值、删除属性。这里我们以__getattribute__的实现为例(读取值)进行分析。属性的__setattr__和__delattr__与前者类似,不同的是禁止修改和删除__dict__属性。def__getattribute__(self,name):lock=object.__getattribute__(self,'_local__lock')lock.acquire()try:_patch(self)returnobject.__getattribute__(self,name)finally:lock.release()函数首先得到了_local__lockThreadLocal变量的属性值(你知道这个变量从哪里来吗,复习一下_localbase),然后用它来保证_patch(self)操作的原子性,用try-finally来保证即使抛出异常它还会释放锁资源,避免线程意外持有永久锁而导致的死锁。现在的问题是_patch到底做了什么?答案还在源码里:def_patch(self):key=object.__getattribute__(self,'_local__key')#ThreadLocal变量标识符d=current_thread().__dict__.get(key)#ThreadLocal变量数据在本线程中ifdisNone:d={}current_thread().__dict__[key]=dobject.__setattr__(self,'__dict__',d)#wehaveanewinstancedict,socallout__init__ifwehaveonecls=type(self)ifcls.__init__isnotobject。__init__:args,kw=object.__getattribute__(self,'_local__args')cls.__init__(self,*args,**kw)else:object.__setattr__(self,'__dict__',d)_patch就是做整个ThreadLocal的实现的核心部分是从当前正在执行的线程对象中获取线程的私有数据,然后将其赋予ThreadLocal变量,也就是本文开头图中的虚线2。这里需要补充以下几点:这里所说的线程私有数据,其实是指可以通过x.name(其中x为ThreadLocal变量)获取到的数据。在主线程中创建ThreadLocal对象后,就有相应的数据(还记得红色虚线的意思吗?)对于那些第一次访问ThreadLocal变量的线程,需要创建一个空字典来存放private数据,然后必须调用变量的初始化函数。还记得_localbase基类中__new__函数设置的属性_local__args吗?它在这里用于初始化。至此,整个源码的核心部分已经理解,只剩下local.__del__进行清理工作了。因为每创建一个ThreadLocal变量,都会将相应的数据添加到进程对象的__dict__中。当变量被回收时,我们需要删除相应线程中保存的相应数据。你从源代码中学到了什么?经过一番努力,终于揭开了ThreadLocal的神秘面纱。整个过程可以说是收获颇丰,下面我就一一说说。我不得不承认,基本的计算机知识非常重要。你要知道什么是进程和线程,CPU的工作机制,什么是操作的原子性,什么是锁,为什么锁使用不当会导致死锁等等。其次,语言层面的知识也是必不可少的。就ThreadLocal的实现而言,如果不了解__new__、__slots__等,就根本不知道该怎么做。因此,深入学习一门语言是很有必要的,否则下面的代码是看不懂的:classdict_test:passd=dict_test()printd.__dict__d.__dict__={'name':'Jack','value':12}printd.name还有一点,高质量的功能实现需要考虑多方面的因素。以ThreadLocal为例,在基类_localbase中使用__slots__来节省空间,使用try_finally保证在异常环境下可以正常释放锁,最后使用__del__及时清除无效信息。最后不得不说,好的文档和评论只是点睛之笔,但是写文档和评论是个技术活,绝对需要不断的学习。
