资讯专栏INFORMATION COLUMN

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

huangjinnan / 517人阅读

摘要:我们知道多线程环境下,每一个线程均可以使用所属进程的全局变量。在线程中使用局部变量则不存在这个问题,因为每个线程的局部变量不能被其他线程访问。

我们知道多线程环境下,每一个线程均可以使用所属进程的全局变量。如果一个线程对全局变量进行了修改,将会影响到其他所有的线程。为了避免多个线程同时对变量进行修改,引入了线程同步机制,通过互斥锁,条件变量或者读写锁来控制对全局变量的访问。

只用全局变量并不能满足多线程环境的需求,很多时候线程还需要拥有自己的私有数据,这些数据对于其他线程来说不可见。因此线程中也可以使用局部变量,局部变量只有线程自身可以访问,同一个进程下的其他线程不可访问。

有时候使用局部变量不太方便,因此 python 还提供了 ThreadLocal 变量,它本身是一个全局变量,但是每个线程却可以利用它来保存属于自己的私有数据,这些私有数据对其他线程也是不可见的。下图给出了线程中这几种变量的存在情况:

全局 VS 局部变量

首先借助一个小程序来看看多线程环境下全局变量的同步问题。

import threading
global_num = 0

def thread_cal():
    global global_num
    for i in xrange(1000):
        global_num += 1

# Get 10 threads, run them and wait them all finished.
threads = []
for i in range(10):
    threads.append(threading.Thread(target=thread_cal))
    threads[i].start()
for i in range(10):
    threads[i].join()

# Value of global variable can be confused.
print global_num

这里我们创建了10个线程,每个线程均对全局变量 global_num 进行1000次的加1操作(循环1000次加1是为了延长单个线程执行时间,使线程执行时被中断切换),当10个线程执行完毕时,全局变量的值是多少呢?答案是不确定。简单来说是因为 global_num += 1 并不是一个原子操作,因此执行过程可能被其他线程中断,导致其他线程读到一个脏值。以两个线程执行 +1 为例,其中一个可能的执行序列如下(此情况下最后结果为1):

多线程中使用全局变量时普遍存在这个问题,解决办法也很简单,可以使用互斥锁、条件变量或者是读写锁。下面考虑用互斥锁来解决上面代码的问题,只需要在进行 +1 运算前加锁,运算完毕释放锁即可,这样就可以保证运算的原子性。

l = threading.Lock()
...
    l.acquire()
    global_num += 1
    l.release()

在线程中使用局部变量则不存在这个问题,因为每个线程的局部变量不能被其他线程访问。下面我们用10个线程分别对各自的局部变量进行1000次加1操作,每个线程结束时打印一共执行的操作次数(每个线程均为1000):

def show(num):
    print threading.current_thread().getName(), num

def thread_cal():
    local_num = 0
    for _ in xrange(1000):
        local_num += 1
    show(local_num)

threads = []
for i in range(10):
    threads.append(threading.Thread(target=thread_cal))
    threads[i].start()

可以看出这里每个线程都有自己的 local_num,各个线程之间互不干涉。

Thread-local 对象

上面程序中我们需要给 show 函数传递 local_num 局部变量,并没有什么不妥。不过考虑在实际生产环境中,我们可能会调用很多函数,每个函数都需要很多局部变量,这时候用传递参数的方法会很不友好。

为了解决这个问题,一个直观的的方法就是建立一个全局字典,保存进程 ID 到该进程局部变量的映射关系,运行中的线程可以根据自己的 ID 来获取本身拥有的数据。这样,就可以避免在函数调用中传递参数,如下示例:

global_data = {}
def show():
    cur_thread = threading.current_thread()
    print cur_thread.getName(), global_data[cur_thread]

def thread_cal():
    global global_data
    cur_thread = threading.current_thread()
    global_data[cur_thread] = 0
    for _ in xrange(1000):
        global_data[cur_thread] += 1
    show()  # Need no local variable.  Looks good.
...

保存一个全局字典,然后将线程标识符作为key,相应线程的局部数据作为 value,这种做法并不完美。首先,每个函数在需要线程局部数据时,都需要先取得自己的线程ID,略显繁琐。更糟糕的是,这里并没有真正做到线程之间数据的隔离,因为每个线程都可以读取到全局的字典,每个线程都可以对字典内容进行更改。

为了更好解决这个问题,python 线程库实现了 ThreadLocal 变量(很多语言都有类似的实现,比如Java)。ThreadLocal 真正做到了线程之间的数据隔离,并且使用时不需要手动获取自己的线程 ID,如下示例:

global_data = threading.local()

def show():
    print threading.current_thread().getName(), global_data.num

def thread_cal():
    global_data.num = 0
    for _ in xrange(1000):
        global_data.num += 1
    show()

threads = []
...

print "Main thread: ", global_data.__dict__ # {}

上面示例中每个线程都可以通过 global_data.num 获得自己独有的数据,并且每个线程读取到的 global_data 都不同,真正做到线程之间的隔离。

ThreadLocal 实现的代码量不多,但是比较难理解,涉及很多 Python 黑魔法,下篇再来分析。那么 ThreadLocal 很完美了?不!Python 的 WSGI 工具库 werkzeug 中有一个更好的 ThreadLocal 实现,甚至支持协程之间的私有数据,实现更加复杂,有机会再分析。

更多阅读

Thread local storage in Python
threading – Manage concurrent threads
Python线程同步机制
Linux多线程与同步
Are local variables in a python function thread safe?

本文由 selfboot 发表于 个人博客,采用署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议。
非商业转载请注明作者及出处。商业转载请联系作者本人。
本文标题为: ThreadLocal之应用篇
本文链接为: http://selfboot.cn/2016/08/22...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/38132.html

相关文章

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

    摘要:在深入理解中的变量上中我们看到的引入,使得可以很方便地在多线程环境中使用局部变量。特别需要注意的是,基类的并不会屏蔽派生类中的创建。到此,整个源码核心部分已经理解的差不多了,只剩下用来执行清除工作。 在 深入理解Python中的ThreadLocal变量(上) 中我们看到 ThreadLocal 的引入,使得可以很方便地在多线程环境中使用局部变量。如此美妙的功能到底是怎样实现的?如果你...

    DataPipeline 评论0 收藏0
  • 深入理解Python中的ThreadLocal变量(下)

    摘要:具体怎么实现的呢,思想其实特别简单,我们在深入理解中的变量上一文的最后有提起过,就是创建一个全局字典,然后将线程或者协程标识符作为,相应线程或协程的局部数据作为。 在上篇我们看到了 ThreadLocal 变量的简单使用,中篇对python中 ThreadLocal 的实现进行了分析,但故事还没有结束。本篇我们一起来看下Werkzeug中ThreadLocal的设计。 Werkzeug...

    dadong 评论0 收藏0
  • Java 总结

    摘要:中的详解必修个多线程问题总结个多线程问题总结有哪些源代码看了后让你收获很多,代码思维和能力有较大的提升有哪些源代码看了后让你收获很多,代码思维和能力有较大的提升开源的运行原理从虚拟机工作流程看运行原理。 自己实现集合框架 (三): 单链表的实现 自己实现集合框架 (三): 单链表的实现 基于 POI 封装 ExcelUtil 精简的 Excel 导入导出 由于 poi 本身只是针对于 ...

    caspar 评论0 收藏0
  • Java面试题必备知识之ThreadLocal

    摘要:方法,删除当前线程绑定的这个副本数字,这个值是的值,普通的是使用链表来处理冲突的,但是是使用线性探测法来处理冲突的,就是每次增加的步长,根据参考资料所说,选择这个数字是为了让冲突概率最小。 showImg(https://segmentfault.com/img/remote/1460000019828633); 老套路,先列举下关于ThreadLocal常见的疑问,希望可以通过这篇学...

    Maxiye 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<