python类方法中的变量_深入理解Python中的ThreadLocal变量(中)

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

cc4c0855ad0beeb5116348854fba6ea4.png

ThreadLocal 实现机制
简单来说,Python 中 ThreadLocal 就是通过下图中的方法,将全局变量伪装成线程局部变量,相信读完本篇文章你会理解图中内容的。

bba38cc78ee79795e5ddee8a04a61b4e.png

在哪里找到源码?
好了,终于要来分析 ThreadLocal 是如何实现的啦,怎么找到它的源码呢?

上一篇中我们只是用过它(from threading import local),从这里只能看出它是在 threading 模块实现的,那么如何找到 threading 模块的源码呢?


如果你在使用 PyCharm,你可以鼠标放到local函数上,按住ctrl鼠标编程小手的链接,点进去找到 local 定义的地方。现在许多 IDE 都有这个功能,可以查看 IDE 的帮助来找到该功能。

接着我们就会发现 local 是这样子的(这里以 python 3.7 为例):

"""Thread-local objects. (Note that this module provides a Python version of the threading.local class. Depending on the version of Python you're using, there may be a faster one available. You should always import the `local` class from `threading`.) Thread-local objects support the management of thread-local data. If you have data that you want to be local to a thread, simply create a thread-local object and use its attributes: >>> mydata = local() >>> mydata.number = 42 >>> mydata.number 42
......
class local:
    __slots__ = '_local__impl', '__dict__'

    def __new__(cls, *args, **kw):
        if (args or kw) and (cls.__init__ is object.__init__):
            raise TypeError("Initialization arguments are not supported")
        self = object.__new__(cls)
        impl = _localimpl()
        impl.localargs = (args, kw)
        impl.locallock = RLock()
        object.__setattr__(self, '_local__impl', impl)
        # We need to create the thread dict in anticipation of
        # __init__ being called, to make sure we don't call it
        # again ourselves.
        impl.create_dict()
        return self
   ......

这个文档的质量非常高,值得我们去学习。所以,再次后悔自己的浅尝辄止了吧,差点错过了这么优秀的文档范文!

将源码私有化

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

class _localimpl:
    """A class managing thread-local dicts"""
    __slots__ = 'key', 'dicts', 'localargs', 'locallock', '__weakref__'

    def __init__(self):
        # The key used in the Thread objects' attribute dicts.
        # We keep it a string for speed but make it unlikely to clash with
        # a "real" attribute.
        self.key = '_threading_local._localimpl.' + str(id(self))
        # { id(Thread) -> (ref(Thread), thread-local dict) }
        self.dicts = {}

    def get_dict(self):
        """Return the dict for the current thread. Raises KeyError if none
        defined."""
        thread = current_thread()
        return self.dicts[id(thread)][1]

    def create_dict(self):
        """Create a new dict for the current thread, and return it."""
        localdict = {}
        key = self.key
        thread = current_thread()
        idt = id(thread)
        def local_deleted(_, key=key):
            # When the localimpl is deleted, remove the thread attribute.
            thread = wrthread()
            if thread is not None:
                del thread.__dict__[key]
    def thread_deleted(_, idt=idt):
            # When the thread is deleted, remove the local dict.
            # Note that this is suboptimal if the thread object gets
            # caught in a reference loop. We would like to be called
            # as soon as the OS-level thread ends instead.
            local = wrlocal()
            if local is not None:
                dct = local.dicts.pop(idt)
        wrlocal = ref(self, local_deleted)
        wrthread = ref(thread, thread_deleted)
        thread.__dict__[key] = wrlocal
        self.dicts[idt] = wrthread, localdict
        return localdict

上面这段代码实现了字典的操作,因为我们通过商品文章可以发现底层就是一个字典。 如何去理解源码

class local:
    __slots__ = '_local__impl', '__dict__'

    def __new__(cls, *args, **kw):
        if (args or kw) and (cls.__init__ is object.__init__):
            raise TypeError("Initialization arguments are not supported")
        self = object.__new__(cls)
        impl = _localimpl()
        impl.localargs = (args, kw)
        impl.locallock = RLock()
        object.__setattr__(self, '_local__impl', impl)
        # We need to create the thread dict in anticipation of
        # __init__ being called, to make sure we don't call it
        # again ourselves.
        impl.create_dict()
        return self

    def __getattribute__(self, name):
        with _patch(self):
            return object.__getattribute__(self, name)

    def __setattr__(self, name, value):
        if name == '__dict__':
            raise AttributeError(
                "%r object attribute '__dict__' is read-only"
                % self.__class__.__name__)
        with _patch(self):
            return object.__setattr__(self, name, value)

    def __delattr__(self, name):
        if name == '__dict__':
            raise AttributeError(
                "%r object attribute '__dict__' is read-only"
                % self.__class__.__name__)
        with _patch(self):
            return object.__delattr__(self, name)


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

  • __slots__
  • __new__
  • __getattribute__/__setattr__/__delattr__

这些是什么?我们有丰富的文档,查文档就对了

python 黑魔法

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

  • 简单来说,python 中创建一个新式类的实例时,首先会调用__new__(cls[, ...])创建实例,如果它成功返回cls类型的对象,然后才会调用__init__来对对象进行初始化。
  • 新式类中我们可以用__slots__指定该类可以拥有的属性名称,这样每个对象就不会再创建dict,从而节省对象占用的空间。特别需要注意的是,基类的__slots__并不会屏蔽派生类中__dict__的创建。
  • 可以通过重载__setattr__,__delattr__和__getattribute__这些方法,来控制自定义类的属性访问(x.name),它们分别对应属性的赋值,删除,读取。
  • __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):
        with _patch(self):
            return object.__getattribute__(self, name)

其中调用了私有方法:_patch(self),然后用它来保证 _patch(self) 操作的原子性,还用 try-finally 保证即使抛出了异常也会释放锁资源,避免了线程意外情况下永久持有锁而导致死锁。现在问题是_patch究竟做了什么?答案还是在源码中:

@contextmanager
def _patch(self):
    impl = object.__getattribute__(self, '_local__impl')
    try:
        dct = impl.get_dict()
    except KeyError:
        dct = impl.create_dict()
        args, kw = impl.localargs
        self.__init__(*args, **kw)
    with impl.locallock:
        object.__setattr__(self, '__dict__', dct)
        yield


做的正是整个ThreadLocal实现中最核心的部分,从当前正在执行的线程对象那里拿到该线程的私有数据,然后将其交给ThreadLocal变量.

到此,整个源码核心部分已经理解的差不多了,只剩下local.__del__用来执行清除工作。因为每次创建一个ThreadLocal 变量,都会在进程对象的__dict__中添加相应的数据,当该变量被回收时,我们需要在相应的线程中删除保存的对应数据。

def __delattr__(self, name):
        if name == '__dict__':
            raise AttributeError(
                "%r object attribute '__dict__' is read-only"
                % self.__class__.__name__)
        with _patch(self):
            return object.__delattr__(self, name)


从源码中学到了什么?
经过一番努力,终于揭开了 ThreadLocal 的神秘面纱,整个过程可以说是收获颇丰,下面一一说来。
不得不承认,计算机基础知识很重要。你得知道进程、线程是什么,CPU 的工作机制,什么是操作的原子性,锁是什么,为什么锁使用不当会导致死锁等等。
其次就是语言层面的知识也必不可少,就ThreadLocal的实现来说,如果对__new__,__slots__等不了解,根本不知道如何去做。

所以,学语言还是要有深度,不然下面的代码都看不懂:

d = dict_test()
print(d.__dict__)
d.__dict__ = {'name': 'Jack', 'value': 12}
print(d.name)


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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值