Python threading Local()函数用法案例详解
作者:码农飞哥 时间:2021-11-27 21:57:02
前言
当多线程访问同一个公共资源时,如果涉及到修改该公共资源的操作就可能会出现由于数据不同步导致的线程安全问题。一般情况下我们可以通过给公共资源加互斥锁的方式来处理该问题。
当然,除非必须将多线程使用的资源设置为公共资源的情况。如果一个资源不需要在多个线程之间共享。我们也可以使用Python threading模块提供的local()方式来避免线程安全问题。
Python threading模块的local()函数跟Java中的ThreadLocal类有诸多类似的地方,感兴趣的小伙伴可以看下Java版的ThreadLoalJava ThreadLocal原理解析以及应用场景分析案例详解
local() 函数是什么?
threading的local()函数主要是用来封装公共资源,使得同一个公共资源在不同线程之间得以隔离。这句话该如何理解呢?举个例子说明下!假设现在有一个大箱子(相当于公共资源),每个人(相当于各个线程)将自己的手机放入这个大箱子里。如果不做任何控制的话,当人们从大箱子中取出手机时极有可能会出现取错的情况(找不到自己当初放入的手机)。而使用local()函数的话,就相当于对这个大箱子进行管理。当每个人放入手机的时候做一个标记(比如在手机上标记所有者的姓名)并隔离放置到箱子中。这样当人们从大箱子中取出手机就能准确的找到自己当初放入的手机。
调用local()函数会生成一个ThreadLocal对象,该对象是所有线程都能访问的,就像上面例子中的大箱子。但是,放入到ThreadLocal对象中的变量则是各个线程所独有的,随便变量名相同,但是指向的值则是完全不同的。
local()函数如何用?
local()函数使用的基本语法是:
import threading
local=threading.local()
第一步就是引入threading模块,第二步就是调用local()函数得到全局的Threadlocal对象。这样说始终是有点干涩,没味道。那么就给代码加点盐吧。还是从那个大箱子说起。
1. 不做标记,不做隔离
第一个示例代码就是所有人将自己的手机放入大箱子里,不做标记,不做隔离。先放入,过一段时间后再取出。
import threading
import time
def set_telephone(telephone):
global global_telephone
global_telephone = telephone
print(threading.current_thread().name + " 放入的手机是", global_telephone)
time.sleep(1)
get_telephone()
def get_telephone():
print(threading.current_thread().name + " 取出的手机是", global_telephone)
if __name__ == '__main__':
for i in range(3):
thread = threading.Thread(target=set_telephone, name='学生' + str(i), args=('手机' + str(i),))
thread.start()
运行结果是:
学生0 放入的手机是 手机0
学生1 放入的手机是 手机1
学生2 放入的手机是 手机2
学生0 取出的手机是 手机2
学生1 取出的手机是 手机2
学生2 取出的手机是 手机2
这里有三个线程,分别模拟学生0,学生1,学生2 将各种的手机赋值给一个全局变量global_telephone(大箱子),然后取全局变量global_telephone中的值。可以看出取出的结果都变成了手机2。这显然没有达到我们的预期结果。这就是不加控制的后果。
2.使用local()函数加以控制
使用local()函数控制的话,就是将全局变量替换成ThreadLoal对象,由他来管理每个线程中的值。
import threading
import time
def set_telephone(telephone):
local.telephone = telephone
print(threading.current_thread().name + " 放入的手机是", local.telephone + "\n")
time.sleep(1)
get_telephone()
def get_telephone():
print(threading.current_thread().name + " 取出的手机是", local.telephone + "\n")
if __name__ == '__main__':
local = threading.local()
for i in range(3):
thread = threading.Thread(target=set_telephone, name='学生' + str(i), args=('手机' + str(i),))
thread.start()
运行结果是:
学生0 放入的手机是 手机0
学生1 放入的手机是 手机1
学生2 放入的手机是 手机2
学生1 取出的手机是 手机1
学生0 取出的手机是 手机0
学生2 取出的手机是 手机2
可以看出每个学生放入的手机和最终取出的手机是一致的。那么threading的local()函数是如何实现这一效果的呢?我们在这里不妨做一个推理。应该是将手机和它的主人做了一层映射关系。根据主人的唯一标识来寻找自己的手机。
3. 模拟实现local()的功能,创建一个箱子
前面我们推测我们需要定义一个全局的字典来存放每个学生各自放入的手机,字典的键是线程ID,值是指定的键值对。示例代码如下:
import threading
import time
global_goods_dict = {}
# {
# "线程ID":{"telephone":"放入的具体手机"},
# "线程ID":{"telephone":"放入的具体手机"},
# "线程ID":{"telephone":"放入的具体手机"}
#
# }
def set_telephone(telephone):
# 获取线程ID
thread_id = threading.get_ident()
global_goods_dict[thread_id] = {}
global_goods_dict[thread_id]["telephone"] = telephone
print(threading.current_thread().name + " 放入的手机是", telephone)
time.sleep(1)
get_telephone()
def get_telephone():
thread_id = threading.get_ident()
print(threading.current_thread().name + " 取出的手机是", global_goods_dict[thread_id]["telephone"])
if __name__ == '__main__':
for i in range(3):
thread = threading.Thread(target=set_telephone, name='学生' + str(i), args=('手机' + str(i),))
thread.start()
运行结果同上,这里定义了一个全局的字典global_goods_dict,字典的键盘是线程ID,这就保证了每个线程只能取到自己设置的数据。字典的值同样是一个字典。这是因为一个线程的要存的值可能不止一个。这里的global_goods_dict[thread_id]["telephone"] = telephone
就等价于上例中的local.telephone = telephone
。这样使用虽然能达到效果,但是使用起来还是有点繁琐。那么能不能想local()函数那样使用起来丝滑呢。
4. 简化代码操作,进一步模拟实现local()函数
我们可以将全局的global_goods_dict字典用一个类封装到一个类中。让该类在自动的设置值
class MyBox:
box = {}
def __setattr__(self, key, value):
thread_id = threading.get_ident()
# 单元格已存在
if thread_id in MyBox.box:
MyBox.box[thread_id][key] = value
else:
MyBox.box[thread_id] = {key: value}
def __getattr__(self, item):
thread_id = threading.get_ident()
return MyBox.box[thread_id][item]
def set_telephone(telephone):
myBox.telephone = telephone
print(threading.current_thread().name + " 放入的手机是", myBox.telephone + "\n")
time.sleep(1)
get_telephone()
def get_telephone():
print(threading.current_thread().name + " 取出的手机是", myBox.telephone + "\n")
if __name__ == '__main__':
myBox = MyBox()
for i in range(3):
thread = threading.Thread(target=set_telephone, name='学生' + str(i), args=('手机' + str(i),))
thread.start()
运行结果同上。这里通过MyBox类封装了一个名为box的字典。该字典的键是当前线程ID,值是赋值的变量名以及值组成的键值对。当执行set_telephone方法的myBox.telephone = telephone
,实际上会调用MyBox的__setattr__
方法,参数key是telephone,参数value是"手机xx"。当调用myBox.telephone时实际上会调用__getattr__
方法,传入的参数item是telephone。取值时首先获取当前线程ID。
来源:https://blog.csdn.net/u014534808/article/details/120106866