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

简介:

在 深入理解Python中的ThreadLocal变量(上) 中我们看到 ThreadLocal 的引入,使得可以很方便地在多线程环境中使用局部变量。如此美妙的功能到底是怎样实现的?如果你对它的实现原理没有好奇心或一探究竟的冲动,那么接下来的内容估计会让你后悔自己的浅尝辄止了。

简单来说,Python 中 ThreadLocal 就是通过下图中的方法,将全局变量伪装成线程局部变量,相信读完本篇文章你会理解图中内容的。(对这张图不眼熟的话,可以回顾下上篇))。

在哪里找到源码?

好了,终于要来分析 ThreadLocal 是如何实现的啦,不过,等等,怎么找到它的源码呢?上一篇中我们只是用过它(from threading import local),从这里只能看出它是在 threading 模块实现的,那么如何找到 threading 模块的源码呢。

如果你在使用 PyCharm,恭喜你,你可以用 View source(OS X 快捷键是 ⌘↓)找到 local 定义的地方。现在许多 IDE 都有这个功能,可以查看 IDE 的帮助来找到该功能。接着我们就会发现 local 是这样子的(这里以 python 2.7 为例):


 
 
  1. # get thread-local implementation, either from the thread 
  2. # module, or from the python fallback 
  3. try: 
  4.     from thread import _local as local 
  5. except ImportError: 
  6.     from _threading_local import local  

嗯,自带解释,非常好。我们要做的是继续往下深挖具体实现,用同样的方法(⌘↓)找 _local 的实现,好像不太妙,没有找到纯 python 实现:


 
 
  1. class _local(object): 
  2.     """ Thread-local data """ 
  3.     def __delattr__(self, name): # real signature unknown; restored from __doc__ 
  4.         """ x.__delattr__('name') <==> del x.name """ 
  5.         pass 
  6.     ...  

没关系,继续来看下_threading_local吧,这下子终于找到了local的纯 python 实现。开始就是很长的一段注释文档,告诉我们这个模块是什么,如何用。这个文档的质量非常高,值得我们去学习。所以,再次后悔自己的浅尝辄止了吧,差点错过了这么优秀的文档范文!

将源码私有化

在具体动手分析这个模块之前,我们先把它拷出来放在一个单独的文件 thread_local.py 中,这样可以方便我们随意肢解它(比如在适当的地方加上log),并用修改后的实现验证我们的一些想法。此外,如果你真的理解了_threading_local.py最开始的一段,你就会发现这样做是多么的有必要。因为python的threading.local不一定是用的_threading_local(还记得class _local(object) 吗?)。

所以如果你用 threading.local 来验证自己对_threading_local.py的理解,你很可能会一头雾水的。不幸的是,我开始就这样干的,所以被下面的代码坑了好久:


 
 
  1. from threading import local, current_thread 
  2. data = local() 
  3. key = object.__getattribute__(data, '_local__key')  
  4. print current_thread().__dict__.get(key
  5. # AttributeError: 'thread._local' object has no attribute '_local__key'  

当然,你可能不理解这里是什么意思,没关系,我只是想强调在 threading.local 没有用到_threading_local.py,你必须要创建一个模块(我将它命名为 thread_local.py)来保存_threading_local里面的内容,然后像下面这样验证自己的想法:


 
 
  1. from threading import current_thread 
  2. from thread_local import local 
  3.  
  4. data = local() 
  5. key = object.__getattribute__(data, '_local__key'
  6. print current_thread().__dict__.get(key)  

如何去理解源码

现在可以静下心来读读这不到两百行的代码了,不过,等等,好像有许多奇怪的内容(黑魔法):

这些是什么?如果你不知道,没关系,千万不要被这些纸老虎吓到,我们有丰富的文档,查文档就对了(这里不建议直接去网上搜相关关键字,最好是先读文档,读完了有疑问再去搜)。

python 黑魔法

下面是我对上面提到的内容的一点总结,如果觉得读的明白,那么可以继续往下分析源码了。如果还有不理解的,再读几遍文档(或者我错了,欢迎指出来)。

  • 简单来说,python 中创建一个新式类的实例时,首先会调用__new__(cls[, ...])创建实例,如果它成功返回cls类型的对象,然后才会调用__init__来对对象进行初始化。
  • 新式类中我们可以用__slots__指定该类可以拥有的属性名称,这样每个对象就不会再创建__dict__,从而节省对象占用的空间。特别需要注意的是,基类的__slots__并不会屏蔽派生类中__dict__的创建。
  • 可以通过重载__setattr__,__delattr__和__getattribute__这些方法,来控制自定义类的属性访问(x.name),它们分别对应属性的赋值,删除,读取。
  • 锁是操作系统中为了保证操作原子性而引入的概念,python 中 RLock是一种可重入锁(reentrant lock,也可以叫作递归锁),Rlock.acquire()可以不被阻塞地多次进入同一个线程。
  • __dict__用来保存对象的(可写)属性,可以是一个字典,或者其他映射对象。

源码剖析

对这些相关的知识有了大概的了解后,再读源码就亲切了很多。为了彻底理解,我们首先回想下平时是如何使用local对象的,然后分析源码在背后的调用流程。这里从定义一个最简单的thread-local对象开始,也就是说当我们写下下面这句时,发生了什么?


 
 
  1. data = local() 

上面这句会调用 _localbase.__new__ 来为data对象设置一些属性(还不知道有些属性是做什么的,不要怕,后面遇见再说),然后将data的属性字典(__dict__)作为当前线程的一个属性值(这个属性的 key 是根据 id(data) 生成的身份识别码)。

这里很值得玩味:在创建ThreadLocal对象时,同时在线程(也是一个对象,没错万物皆对象)的属性字典__dict__里面保存了ThreadLocal对象的属性字典。还记得文章开始的图片吗,红色虚线就表示这个操作。

接着我们考虑在线程 Thread-1 中对ThreadLocal变量进行一些常用的操作,比如下面的一个操作序列:


 
 
  1. data.name = "Thread 1(main)" # 调用 __setattr__ 
  2. print data.name     # 调用 __getattribute__ 
  3. del data.name       # 调用 __delattr__ 
  4. print data.__dict__ 
  5. # Thread 1(main) 
  6. # {}  

那么背后又是如何操作的呢?上面的操作包括了给属性赋值,读属性值,删除属性。这里我们以__getattribute__的实现为例(读取值)进行分析,属性的__setattr__和__delattr__和前者差不多,区别在于禁止了对__dict__属性的更改以及删除操作。


 
 
  1. def __getattribute__(self, name): 
  2.     lock = object.__getattribute__(self, '_local__lock'
  3.     lock.acquire() 
  4.     try: 
  5.         _patch(self) 
  6.         return object.__getattribute__(self, name
  7.     finally: 
  8.         lock.release()  

函数中首先获得了ThreadLocal变量的_local__lock属性值(知道这个变量从哪里来的吗,回顾下_localbase吧),然后用它来保证_patch(self) 操作的原子性,还用 try-finally 保证即使抛出了异常也会释放锁资源,避免了线程意外情况下永久持有锁而导致死锁。现在问题是_patch究竟做了什么?答案还是在源码中:


 
 
  1. def _patch(self): 
  2.     key = object.__getattribute__(self, '_local__key')  # ThreadLocal变量 的标识符 
  3.     d = current_thread().__dict__.get(key)  # ThreadLocal变量在该线程下的数据 
  4.     if d is None: 
  5.         d = {} 
  6.         current_thread().__dict__[key] = d 
  7.         object.__setattr__(self, '__dict__', d) 
  8.  
  9.         # we have a new instance dict, so call out __init__ if we have one 
  10.         cls = type(self) 
  11.         if cls.__init__ is not object.__init__: 
  12.             args, kw = object.__getattribute__(self, '_local__args'
  13.             cls.__init__(self, *args, **kw) 
  14.     else
  15.         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__等不了解,根本不知道如何去做。所以,学语言还是要有深度,不然下面的代码都看不懂:


 
 
  1. class dict_test: 
  2.     pass 
  3.  
  4. d = dict_test() 
  5. print d.__dict__ 
  6. d.__dict__ = {'name''Jack''value': 12} 
  7. print d.name  

还有就是高质量的功能实现需要考虑各方各面的因素,以ThreadLocal 为例,在基类_localbase中用__slots__节省空间,用try_finally保证异常环境也能正常释放锁,最后还用__del__来及时的清除无效的信息。

最后不得不说,好的文档和注释简直就是画龙点睛,不过写文档和注释是门技术活,绝对需要不断学习的。


作者:selfboot

来源:51CTO

相关文章
|
22天前
|
存储 Linux iOS开发
Python入门:2.注释与变量的全面解析
在学习Python编程的过程中,注释和变量是必须掌握的两个基础概念。注释帮助我们理解代码的意图,而变量则是用于存储和操作数据的核心工具。熟练掌握这两者,不仅能提高代码的可读性和维护性,还能为后续学习复杂编程概念打下坚实的基础。
Python入门:2.注释与变量的全面解析
|
1月前
|
存储 Python 容器
python之变量的使用
Python 中变量是对象的引用,赋值即为指向内存中对象。创建对象时,解释器分配内存,引用计数管理内存回收。Python 是动态类型语言,变量类型在运行时确定。对象分为可变与不可变,前者可修改内部状态,后者则不行。命名空间管理变量作用域,确保不同区域的变量独立。
|
3月前
|
Python
[oeasy]python050_如何删除变量_del_delete_variable
本文介绍了Python中如何删除变量,通过`del`关键字实现。首先回顾了变量的声明与赋值,说明变量在声明前是不存在的,通过声明赋予其生命和初始值。使用`locals()`函数可查看当前作用域内的所有本地变量。进一步探讨了变量的生命周期,包括自然死亡(程序结束时自动释放)和手动删除(使用`del`关键字)。最后指出,删除后的变量将无法在当前作用域中被访问,并提供了相关示例代码及图像辅助理解。
132 68
|
2月前
|
人工智能 Unix Java
[oeasy]python059变量命名有什么规则_惯用法_蛇形命名法_name_convention_snake
本文探讨了Python中变量命名的几种常见方式,包括汉语拼音变量名、蛇形命名法(snake_case)和驼峰命名法(CamelCase)。回顾上次内容,我们主要讨论了使用下划线替代空格以提高代码可读性。实际编程中,当变量名由多个单词组成时,合理的命名惯例变得尤为重要。
141 9
|
3月前
|
Shell Python
[oeasy]python049_[词根溯源]locals_现在都定义了哪些变量
本文介绍了Python中`locals()`函数的使用方法及其在调试中的作用。通过回顾变量赋值、连等赋值、解包赋值等内容,文章详细解释了如何利用`locals()`函数查看当前作用域内的本地变量,并探讨了变量声明前后以及导入模块对本地变量的影响。最后,文章还涉及了一些与“local”相关的英语词汇,如`locate`、`allocate`等,帮助读者更好地理解“本地”概念在编程及日常生活中的应用。
52 9
|
4月前
|
Python
Python三引号用法与变量详解
本文详细介绍了Python中三引号(`&quot;&quot;&quot;` 或 `&#39;&#39;&#39;`)的用法,包括其基本功能、如何在多行字符串中使用变量(如f-string、str.format()和%操作符),以及实际应用示例,帮助读者更好地理解和运用这一强大工具。
230 2
|
5月前
|
Python
【10月更文挑战第5天】「Mac上学Python 8」基础篇2 - 变量深入详解
本篇将详细介绍Python中变量的使用方式和进阶操作,涵盖变量的输入与输出、变量的多重赋值、变量的内存地址管理以及变量的传递和交换等操作。通过本篇的学习,用户将对变量的使用有更深入的理解,并能灵活运用变量进行各种编程操作。
73 1
【10月更文挑战第5天】「Mac上学Python 8」基础篇2 - 变量深入详解
|
4月前
|
人工智能 Python
[oeasy]python039_for循环_循环遍历_循环变量
本文回顾了上一次的内容,介绍了小写和大写字母的序号范围,并通过 `range` 函数生成了 `for` 循环。重点讲解了 `range(start, stop)` 的使用方法,解释了为什么不会输出 `stop` 值,并通过示例展示了如何遍历小写和大写字母的序号。最后总结了 `range` 函数的结构和 `for` 循环的使用技巧。
54 4
|
4月前
|
机器学习/深度学习 存储 数据挖掘
Python 编程入门:理解变量、数据类型和基本运算
【10月更文挑战第43天】在编程的海洋中,Python是一艘易于驾驭的小船。本文将带你启航,探索Python编程的基础:变量的声明与使用、丰富的数据类型以及如何通过基本运算符来操作它们。我们将从浅显易懂的例子出发,逐步深入到代码示例,确保即使是零基础的读者也能跟上步伐。准备好了吗?让我们开始吧!
65 0
|
5月前
|
存储 编译器 Python
Python--变量、输出与输入
【10月更文挑战第5天】
55 5

热门文章

最新文章