在 深刻理解Python中的ThreadLocal变量(上) 中咱们看到 ThreadLocal 的引入,使得能够很方便地在多线程环境中使用局部变量。如此美妙的功能究竟是怎样实现的?若是你对它的实现原理没有好奇心或一探究竟的冲动,那么接下来的内容估计会让你后悔本身的浅尝辄止了。html
简单来讲,Python 中 ThreadLocal 就是经过下图中的方法,将全局变量假装成线程局部变量,相信读完本篇文章你会理解图中内容的。(对这张图不眼熟的话,能够回顾下上篇))。python
好了,终于要来分析 ThreadLocal 是如何实现的啦,不过,等等,怎么找到它的源码呢?上一篇中咱们只是用过它(from threading import local
),从这里只能看出它是在 threading 模块实现的,那么如何找到 threading 模块的源码呢。c++
若是你在使用 PyCharm,恭喜你,你能够用 View source
(OS X 快捷键是 ⌘↓)找到 local 定义的地方。如今许多 IDE 都有这个功能,能够查看 IDE 的帮助来找到该功能。接着咱们就会发现 local 是这样子的(这里以 python 2.7 为例):git
# get thread-local implementation, either from the thread # module, or from the python fallback try: from thread import _local as local except ImportError: from _threading_local import local
嗯,自带解释,很是好。咱们要作的是继续往下深挖具体实现,用一样的方法(⌘↓)找 _local 的实现,好像不太妙,没有找到纯 python 实现:github
class _local(object): """ Thread-local data """ def __delattr__(self, name): # real signature unknown; restored from __doc__ """ x.__delattr__('name') <==> del x.name """ pass ...
不要紧,继续来看下_threading_local吧,这下子终于找到了local的纯 python 实现。开始就是很长的一段注释文档,告诉咱们这个模块是什么,如何用。这个文档的质量很是高,值得咱们去学习。因此,再次后悔本身的浅尝辄止了吧,差点错过了这么优秀的文档范文!多线程
在具体动手分析这个模块以前,咱们先把它拷出来放在一个单独的文件 thread_local.py
中,这样能够方便咱们随意肢解它(好比在适当的地方加上log),并用修改后的实现验证咱们的一些想法。此外,若是你真的理解了_threading_local.py最开始的一段,你就会发现这样作是多么的有必要。由于python的threading.local不必定是用的_threading_local(还记得class _local(object) 吗?)。函数
因此若是你用 threading.local 来验证本身对_threading_local.py的理解,你极可能会一头雾水的。不幸的是,我开始就这样干的,因此被下面的代码坑了很久:post
from threading import local, current_thread data = local() key = object.__getattribute__(data, '_local__key') print current_thread().__dict__.get(key) # AttributeError: 'thread._local' object has no attribute '_local__key'
固然,你可能不理解这里是什么意思,不要紧,我只是想强调在 threading.local 没有用到_threading_local.py,你必需要建立一个模块(我将它命名为 thread_local.py)来保存_threading_local里面的内容,而后像下面这样验证本身的想法:学习
from threading import current_thread from thread_local import local data = local() key = object.__getattribute__(data, '_local__key') print current_thread().__dict__.get(key)
如今能够静下心来读读这不到两百行的代码了,不过,等等,好像有许多奇怪的内容(黑魔法):ui
这些是什么?若是你不知道,不要紧,千万不要被这些纸老虎吓到,咱们有丰富的文档,查文档就对了(这里不建议直接去网上搜相关关键字,最好是先读文档,读完了有疑问再去搜)。
下面是我对上面提到的内容的一点总结,若是以为读的明白,那么能够继续往下分析源码了。若是还有不理解的,再读几遍文档(或者我错了,欢迎指出来)。
简单来讲,python 中建立一个新式类的实例时,首先会调用__new__(cls[, ...])
建立实例,若是它成功返回cls类型的对象,而后才会调用__init__来对对象进行初始化。
新式类中咱们能够用__slots__指定该类能够拥有的属性名称,这样每一个对象就不会再建立__dict__,从而节省对象占用的空间。特别须要注意的是,基类的__slots__并不会屏蔽派生类中__dict__的建立。
能够经过重载__setattr__,__delattr__和__getattribute__
这些方法,来控制自定义类的属性访问(x.name),它们分别对应属性的赋值,删除,读取。
锁是操做系统中为了保证操做原子性而引入的概念,python 中 RLock是一种可重入锁(reentrant lock,也能够叫做递归锁),Rlock.acquire()能够不被阻塞地屡次进入同一个线程。
__dict__
用来保存对象的(可写)属性,能够是一个字典,或者其余映射对象。
对这些相关的知识有了大概的了解后,再读源码就亲切了不少。为了完全理解,咱们首先回想下平时是如何使用local对象的,而后分析源码在背后的调用流程。这里从定义一个最简单的thread-local对象开始,也就是说当咱们写下下面这句时,发生了什么?
data = local()
上面这句会调用 _localbase.__new__
来为data对象设置一些属性(还不知道有些属性是作什么的,不要怕,后面碰见再说),而后将data的属性字典(__dict__
)做为当前线程的一个属性值(这个属性的 key 是根据 id(data) 生成的身份识别码)。
这里很值得玩味:在建立ThreadLocal对象时,同时在线程(也是一个对象,没错万物皆对象)的属性字典__dict__
里面保存了ThreadLocal对象的属性字典。还记得文章开始的图片吗,红色虚线就表示这个操做。
接着咱们考虑在线程 Thread-1 中对ThreadLocal变量进行一些经常使用的操做,好比下面的一个操做序列:
data.name = "Thread 1(main)" # 调用 __setattr__ print data.name # 调用 __getattribute__ del data.name # 调用 __delattr__ print data.__dict__ # Thread 1(main) # {}
那么背后又是如何操做的呢?上面的操做包括了给属性赋值,读属性值,删除属性。这里咱们以__getattribute__的实现为例(读取值)进行分析,属性的__setattr__和__delattr__和前者差很少,区别在于禁止了对__dict__属性的更改以及删除操做。
def __getattribute__(self, name): lock = object.__getattribute__(self, '_local__lock') lock.acquire() try: _patch(self) return object.__getattribute__(self, name) finally: lock.release()
函数中首先得到了ThreadLocal变量的_local__lock
属性值(知道这个变量从哪里来的吗,回顾下_localbase吧),而后用它来保证 _patch(self)
操做的原子性,还用 try-finally 保证即便抛出了异常也会释放锁资源,避免了线程意外状况下永久持有锁而致使死锁。如今问题是_patch究竟作了什么?答案仍是在源码中:
def _patch(self): key = object.__getattribute__(self, '_local__key') # ThreadLocal变量 的标识符 d = current_thread().__dict__.get(key) # ThreadLocal变量在该线程下的数据 if d is None: d = {} current_thread().__dict__[key] = d object.__setattr__(self, '__dict__', d) # we have a new instance dict, so call out __init__ if we have one cls = type(self) if cls.__init__ is not object.__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变量的线程来讲,须要建立一个空的字典来保存私有数据,而后还要调用该变量的初始化函数。
还记得_localbase基类里__new__函数设置的属性 _local__args 吗?在这里被用来进行初始化。
到此,整个源码核心部分已经理解的差很少了,只剩下local.__del__
用来执行清除工做。由于每次建立一个ThreadLocal 变量,都会在进程对象的__dict__中添加相应的数据,当该变量被回收时,咱们须要在相应的线程中删除保存的对应数据。
通过一番努力,终于揭开了 ThreadLocal 的神秘面纱,整个过程能够说是收获颇丰,下面一一说来。
不得不认可,计算机基础知识很重要。你得知道进程、线程是什么,CPU 的工做机制,什么是操做的原子性,锁是什么,为何锁使用不当会致使死锁等等。
其次就是语言层面的知识也必不可少,就ThreadLocal的实现来讲,若是对__new__,__slots__等不了解,根本不知道如何去作。因此,学语言仍是要有深度,否则下面的代码都看不懂:
class dict_test: pass d = dict_test() print d.__dict__ d.__dict__ = {'name': 'Jack', 'value': 12} print d.name
还有就是高质量的功能实现须要考虑各方各面的因素,以ThreadLocal 为例,在基类_localbase中用__slots__节省空间,用try_finally保证异常环境也能正常释放锁,最后还用__del__来及时的清除无效的信息。
最后不得不说,好的文档和注释简直就是画龙点睛,不过写文档和注释是门技术活,绝对须要不断学习的。
Python's use of __new__ and __init__?
Understanding __new__ and __init__
Usage of __slots__?
weakref – Garbage-collectable references to objects
How do I find the source code of a function in Python?
How do I find the location of Python module sources?
Is self.__dict__.update(**kwargs) good or poor style?
Doc: weakref — Weak references
python class 全面分析
我是如何阅读开源项目的源代码的
高效阅读源代码指南
如何阅读程序源代码?
如何看懂源代码--(分析源代码方法)
本文由selfboot 发表于我的博客,采用署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议。
非商业转载请注明做者及出处。商业转载请联系做者本人
本文标题为:深刻理解Python中的ThreadLocal变量(中)
本文连接为:http://selfboot.cn/2016/08/26...