• Python thread local


      由于GIL的原因,笔者在日常开发中几乎没有用到python的多线程。如果需要并发,一般使用多进程,对于IO Bound这种情况,使用协程也是不错的注意。但是在python很多的网络库中,都支持多线程,基本上都会使用到threading.local。在python中threading.local用来表示线程相关的数据,线程相关指的是这个属性再各个线程中是独立的 互不影响,先来看一个最简答的例子:
       
     1 class Widgt(object):
     2     pass
     3 
     4 import threading
     5 def test():
     6     local_data = threading.local()
     7     # local_data = Widgt()
     8     local_data.x = 1
     9 
    10     def thread_func():
    11         print('Has x in new thread: %s' % hasattr(local_data, 'x'))
    12         local_data.x = 2
    13 
    14     t = threading.Thread(target = thread_func)
    15     t.start()
    16     t.join()
    17     print('x in pre thread is %s' % local_data.x)
    18 
    19 if __name__ == '__main__':
    20     test()
    输出:
        Has x in new thread: False
        x in pre thread is 1
     
      可以看到,在新的线程中 local_data 并没有x属性,并且在新线程中的赋值并不会影响到其他线程。也可以稍微改改代码,去掉第7行的注释,local_data就变成了线程共享的变量。
     
      local怎么实现的呢 在threading.py 代码如下:
      
    1 try:
    2     from thread import _local as local
    3 except ImportError:
    4     from _threading_local import local
      可以看到,local是python的buildin class,同时也提供了一个纯python版本的参考实现,在_threading_local.py,我们来看看代码(代码不全 省略了几个函数):
      
     1 class _localbase(object):
     2     __slots__ = '_local__key', '_local__args', '_local__lock'
     3 
     4     def __new__(cls, *args, **kw):
     5         self = object.__new__(cls)
     6         key = '_local__key', 'thread.local.' + str(id(self)) # 产生一个key,这个key在同一个进程的多个线程中是一样的
     7         object.__setattr__(self, '_local__key', key)
     8         object.__setattr__(self, '_local__args', (args, kw))
     9         object.__setattr__(self, '_local__lock', RLock()) # 可重入的锁
    10 
    11         if (args or kw) and (cls.__init__ is object.__init__):
    12             raise TypeError("Initialization arguments are not supported")
    13 
    14         # We need to create the thread dict in anticipation of
    15         # __init__ being called, to make sure we don't call it
    16         # again ourselves.
    17         dict = object.__getattribute__(self, '__dict__')
    18         current_thread().__dict__[key] = dict   # 在current_thread这个线程唯一的对象的—__dict__中加入 key
    19 
    20         return self
    21 
    22 def _patch(self):
    23     key = object.__getattribute__(self, '_local__key')
    24     d = current_thread().__dict__.get(key)    # 注意 current_thread 在每一个线程是不同的对象
    25     if d is None: # 在新的线程第一次调用时
    26         d = {}    # 一个空的dict !!!
    27         current_thread().__dict__[key] = d 
    28         object.__setattr__(self, '__dict__', d) # 将实例的__dict__赋值为 线程独立的一个字典
    29 
    30         # we have a new instance dict, so call out __init__ if we have
    31         # one
    32         cls = type(self)
    33         if cls.__init__ is not object.__init__:
    34             args, kw = object.__getattribute__(self, '_local__args')
    35             cls.__init__(self, *args, **kw)
    36     else:
    37         object.__setattr__(self, '__dict__', d)
    38 
    39 class local(_localbase):
    40 
    41     def __getattribute__(self, name):
    42         lock = object.__getattribute__(self, '_local__lock')
    43         lock.acquire()
    44         try:
    45             _patch(self) # 这条语句执行之后,self.__dict__ 被修改成了线程独立的一个dict
    46             return object.__getattribute__(self, name)
    47         finally:
    48             lock.release()
       代码中 已经加入了注释,便于理解。总结就是,在每个线程中增加一个独立的dict(通过current_thread()这个线程独立的对象),然后每次对local实例增删改查的时候,进行__dict__的替换。我们看看测试代码:
      
     1 import threading
     2 from _threading_local import local
     3 def test():
     4     local_data = local()
     5     local_data.x = 1
     6     print 'id of local_data', id(local_data)
     7 
     8     def thread_func():
     9         before_keys = threading.current_thread().__dict__.keys()
    10         local_data.x = 2
    11         after = threading.current_thread().__dict__
    12         # print set(after.keys())  - set(before.keys())
    13         print [(e, v) for (e, v) in after.iteritems() if e not in before_keys]
    14 
    15     t = threading.Thread(target = thread_func)
    16     t.start()
    17     t.join()
    18     print('x in pre thread is %s' % local_data.x)
    19 
    20 if __name__ == '__main__':
    21     test()

     输出:

      id of local_data 40801456
      [(('_local__key', 'thread.local.40801456'), {'x': 2})]

      从输出可以看到,在这次运行总,local_data的id是40801456,在每个线程中都是一样的。在新的线程(thread_func函数)中访问local_data对象之前,current_thread()返回的对象是没有__local_key的,在第10行访问的时候会增加这个属性(_patch函数中)。
      
      在gevent中,也有一个类叫local,其作用是提供协程独立的数据。PS:gevent中提供了几乎与python原生协程一样的数据结构,如Event、Semaphore、Local,而且,gevent的代码和文档中也自称为“thread”,这点需要注意。gevent.local的实现借鉴了上面介绍的_threading_local.py, 区别在于,_threading_local.local 将线程独立的数据存放在current_thread()中,而gevent.local将协程独立的数据存放在greenlet.getcurrent()中。
     
       最后,如果在代码中使用了gevent.monkey.patch_all(),那么python原生的threading.local将会被替换成gevent.local.local。之前在看bottle的代码的时候,发现里面都是使用的threading.local,当时也对monkey_patch具体patch了那些模块不了解,于是就想如果使用gevent是否会出错呢,结果测试了很久都发现没问题,直到重新细看bottle源码才发现原因所在。代码如下:
      
     1 class GeventServer(ServerAdapter):
     2     """ Untested. Options:
     3 
     4         * See gevent.wsgi.WSGIServer() documentation for more options.
     5     """
     6 
     7     def run(self, handler):
     8         from gevent import pywsgi, local
     9         if not isinstance(threading.local(), local.local): #注意这里
    10             msg = "Bottle requires gevent.monkey.patch_all() (before import)"
    11             raise RuntimeError(msg)
    12         if self.quiet:
    13             self.options['log'] = None
    14         address = (self.host, self.port)
    15         server = pywsgi.WSGIServer(address, handler, **self.options)
    16         if 'BOTTLE_CHILD' in os.environ:
    17             import signal
    18             signal.signal(signal.SIGINT, lambda s, f: server.stop())
    19         server.serve_forever()

      这个小插曲其实也反映了monkey-patch的一些优势与劣势。其优势在于不对源码修改就能改变运行时行为,提高性能;同时 ,对于缺乏经验或者对patch细节不了解的人来说,会带来静态代码与运行结果之间的认知差异。

     
    references:
     
     
     
     
     
     
     
     
     
     
  • 相关阅读:
    不注意的小问题
    Hibernate、Spring和Struts工作原理及使用理由
    正则表达式贪婪与非贪婪模式
    Springmvc构造RESTful详细讲解
    正则表达式贪婪与非贪婪模式1
    BOJ二叉排序树的后序遍历
    qsort()应用大全
    辗转相除法求最大公约数(C语言)
    九度题目1014:排名 (结构体多级排序)
    BOJ第三题:二叉树前序遍历
  • 原文地址:https://www.cnblogs.com/xybaby/p/6420873.html
Copyright © 2020-2023  润新知