【Python从入门到精通】(二十三)Python threading Local()函数用法:返回线程局部变量

举报
码农飞哥 发表于 2021/09/08 16:53:43 2021/09/08
【摘要】 threading local

您好,我是码农飞哥,感谢您阅读本文,欢迎一键三连哦
本文重点介绍threading Local()函数的用法。
干货满满,建议收藏,需要用到时常看看。 小伙伴们如有问题及需要,欢迎踊跃留言哦~ ~ ~。

@[TOC]

前言

当多线程访问同一个公共资源时,如果涉及到修改该公共资源的操作就可能会出现由于数据不同步导致的线程安全问题。一般情况下我们可以通过给公共资源加互斥锁的方式来处理该问题。

当然,除非必须将多线程使用的资源设置为公共资源的情况。如果一个资源不需要在多个线程之间共享。我们也可以使用Python threading模块提供的local()方式来避免线程安全问题。
Python threading模块的local()函数跟Java中的ThreadLocal类有诸多类似的地方,感兴趣的小伙伴可以看下Java版的ThreadLoal第十八篇: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。
在这里插入图片描述
在这里插入图片描述

总结

本文从实际例子出发详细介绍了threading模块的local()函数的使用。

我是码农飞哥,再次感谢您读完本文

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。