热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

python中本地线程ThreadLocal解释和简单自定义示例

前置关于本地线程(线程本地变量,线程本地存

前置

关于本地线程(线程本地变量,线程本地存贮),如果你之前有使用过Flask框架,肯定会好奇,为什么在多线的模式的情况,每个线程都能保留自己一份独立的参数,而不会被其他线程的参数影响,(每个请求上下文中都有自己的内容)。

理论来说对于(单核多线程模式下)Flask Web应用来说,每一个请求就是一个独立的线程。请求之间的信息要完全隔离,避免冲突,这就需要用到Thread Local。ThreadLocal 中每一个变量中都会创建一个副本,每个线程都可以访问自己内部的副本变量。

比如:flask中我们的一个HTTP请求,都能保持独立自己的参数信息,flask中全局变量改为dict, 每一个线程都有对应的自己的key, 并将request作为value存放,

PS:通常我们的flask运行的是,一般有几个模式

  • 单进程单线程
  • 多进程多线程
  • 单进程多线程
  • 单进程多协程

但是在我们的ThreadLocal对于协程的方式是不支持的!后续再梳理一下,在flask中,听闻作者是自己自定义一个Local用于对协程的支持!(比较我们的并发模式有很多种)下我们的Local一些知识点。

PS:python 自带的ThreadLocal只能实现基于线程的并发

关于ThreadLocal

  • 在python中一个ThreadLocal是一个全局变量,但是它是一种特殊的对象,它的状态对线程隔离。

  • 一个线程中的局部变量只有线程自身可以访问,同一个进程下的其他线程不可访问,每个线程都只能读写自己线程的独立副本,这样可以做到线程隔离,做相关数据防污染!这样就可以利用它来保存属于线程自己内部的私有数据。

意思就是:每个线程对一个Thread Local对象的修改都不会影响其他的线程。

ThreadLocal对象实现原理:以线程的ID来保存多份状态字典。

PS:当然也可以改为我们的协程的!指需要内部改为协程的ID!

简单示例:

import threading

# 获取Thread Local对象
local_storage = threading.local()
local_storage.number = 100
local_storage.request = "请求对象"
print('初始化',local_storage.number)
print('初始化',local_storage.request)
# 定义修改变量的线程
class TestThread(threading.Thread):
    def run(self):
        print('开始修改local_storage.number')
        local_storage.number = 10002
        print('开始修改local_storage.request')
        local_storage.request = "我是线程内独立的一个请求对象"
        import time
        time.sleep(1)
        print(local_storage.number)
        print(local_storage.request)
        print(">"*20)

another = TestThread()
another.start()  # 打印2
another.join()
print('在主线程里面的值并没有被修改: ',local_storage.number)  # 但是在主线程里面的值并没有被修改 打印1
print('在主线程里面的值并没有被修改: ',local_storage.request)  # 但是在主线程里面的值并没有被修改 打印1

输出结果为:

初始化 100
初始化 请求对象
开始修改local_storage.number
开始修改local_storage.request
10002
我是线程内独立的一个请求对象
>>>>>>>>>>>>>>>>>>>>
在主线程里面的值并没有被修改:  100
在主线程里面的值并没有被修改:  请求对象

自己模拟顶一个类似的ThreadLocal,之前,关于字节码执行的是,其实我们的了解到过了关于python字典是否是线程安全的问题。

如果验证字典是否是线程的安装的,主要是看原子操作,写操作的是否分多个STORE_SUBSCR字节码。

分析字典的添加是否是线程安全显示 acton() 的反汇编:


storage = {}

def acton():
    global  storage
    storage={"xiaozhong",1121}


import dis

dis.dis(acton)

输出的结果为:

 18           0 LOAD_CONST               1 ('xiaozhong')
              2 LOAD_CONST               2 (1121)
              4 BUILD_SET                2
              6 STORE_GLOBAL             0 (storage)
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE


字节码指令参考官网:https://docs.python.org/zh-cn/3/library/dis.html
从上面分析看字典设置字典值的时候是线程安全。

  • LOAD_CONST(consti)
    将 co_consts[consti] 推入栈顶。

  • BUILD_SET(count)
    类似于 BUILD_TUPLE 但会创建一个集合。

  • STORE_GLOBAL(namei)
    类似于 STORE_NAME 但会将 name 存储为全局变量。

  • RETURN_VALUE
    返回 TOS 到函数的调用者。

所以我们可以使用字典的形式来模拟:

from _thread import get_ident
import threading
import time


class MyThreadLocal(object):
    def __init__(self):
        # 自定义用于存贮对象的字典
        self.storage = {}
        # 获取当前线程的ID对象
        self.get_ident = get_ident

    #
    def set(self, k, v):
        # 获取到线下的IDident
        ident = self.get_ident()
        # print("线程自己的ID",ident)
        # 判断是否已经存在的
        if ident in self.storage:
            # 存在就创建内部新的字典
            self.storage[ident][k] = v
        else:
            # 不存在就创建,且创建新的内部字典用于存在自己内部的参数变量
            self.storage[ident]={k: v}

    def get(self, k):
        ident = self.get_ident()
        itemobj = self.storage.get(ident)
        if not itemobj:
            return None
        return itemobj.get(k, None)


local_values = MyThreadLocal()


def action_task(num):
    # 没一个线程保存自己的额内部的变量的信息
    local_values.set('name', num)
    # 设置线程内部需要的定义自己的变量
    local_values.set('reques''大爷的%s'%(num))
    time.sleep(2)
    # print(local_values.get('name'), threading.current_thread().name)


for i in range(3):
    # th = threading.Thread(target=task, args=(i,))
    th = threading.Thread(target=action_task, args=(i,), name='线程%s' % i)
    th.start()

print(local_values.__dict__)

输出结果为:

{'storage': {9296: {'name': 0, 'reques': '大爷的0'}, 15760: {'name': 1, 'reques': '大爷的1'}, 12660: {'name': 2, 'reques': '大爷的2'}}, 'get_ident':}

修改使用python魔法函数进行实现,补充几个魔法函数的说明:

class Test:
    def __getattr__(self, key):
        value = self[key]
        if isinstance(value, dict):
            value = Test(value)
        return value

    def __setattr__(self, key, value):
        self.__dict__[key] = value

    def __getitem__(self, item):
        return self.__dict__[item]

    def __setitem__(self, key, value):
        self.__dict__[key] = value


s = Test()
s.name = "后端"# 调用__setattr__ 方法
print(s.name) # 会调用__getattr__方法
s.age = 991   # 调用__setattr__ 方法
print(s.age)
print(s.name) # 会调用__getattr__方法
print(s['age'])  # 调用 __getitem__方法 输出1
s['name'] = 'tom'  # 调用 __setitem__ 方法


修改我们的自定义的本地线程类:参考我们的flask内部自定义的本地线程类的源码

flask内部自定义的本地线程类的源码:

try:
    from greenlet import getcurrent as get_ident
except ImportError:
    try:
        from thread import get_ident
    except ImportError:
        from _thread import get_ident


def release_local(local):
    """Releases the contents of the local for the current context.
    This makes it possible to use locals without a manager.

    Example::

        >>> loc = Local()
        >>> loc.foo = 42
        >>> release_local(loc)
        >>> hasattr(loc, 'foo')
        False

    With this function one can release :class:`Local` objects as well
    as :class:`LocalStack` objects.  However it is not possible to
    release data held by proxies that way, one always has to retain
    a reference to the underlying local object in order to be able
    to release it.

    .. versionadded:: 0.6.1
    "
""
    local.__release_local__()


class Local(object):
    __slots__ = ("__storage__""__ident_func__")

    def __init__(self):
        object.__setattr__(self, "__storage__", {})
        object.__setattr__(self, "__ident_func__", get_ident)

    def __iter__(self):
        return iter(self.__storage__.items())

    def __call__(self, proxy):
        """Create a proxy for a name."""
        return LocalProxy(self, proxy)

    def __release_local__(self):
        self.__storage__.pop(self.__ident_func__(), None)

    def __getattr__(self, name):
        try:
            return self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        ident = self.__ident_func__()
        storage = self.__storage__
        try:
            storage[ident][name] = value
        except KeyError:
            storage[ident] = {name: value}

    def __delattr__(self, name):
        try:
            del self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)


我们自己定义的源码:

from _thread import get_ident
import threading
import time


class MyThreadLocal(object):
    def __init__(self):
        # 自定义用于存贮对象的字典
        object.__setattr__(self,'storage',{})
        # self.storage = {}

    def __setattr__(self, key, value):
        # 获取到线下的IDident
        ident = get_ident()
        # print("线程自己的ID",ident)
        # 判断是否已经存在的
        if ident in self.storage:
            # 存在就创建内部新的字典
            self.storage[ident][key] = value
        else:
            # 不存在就创建,且创建新的内部字典用于存在自己内部的参数变量
            self.storage[ident] = {key: value}

    def __getattr__(self, key):
        ident = self.get_ident()
        return self.storage.get(ident)[key]


    def __delattr__(self, key):
        try:
            ident = self.get_ident()
            del self.storage[ident][key]
        except KeyError:
            raise AttributeError(key)




local_values = MyThreadLocal()


def action_task(num):
    # 没一个线程保存自己的额内部的变量的信息
    local_values.name ="你没的%s"%(num)
    local_values.reques = '大爷的%s'%(num)
    # 设置线程内部需要的定义自己的变量
    time.sleep(2)
    # print(local_values.get('name'), threading.current_thread().name)


for i in range(3):
    # th = threading.Thread(target=task, args=(i,))
    th = threading.Thread(target=action_task, args=(i,), name='线程%s' % i)
    th.start()

print(local_values.__dict__)

输出结果:

{'storage': {8200: {'name': '你没的0', 'reques': '大爷的0'}, 16696: {'name': '你没的1', 'reques': '大爷的1'}, 10464: {'name': '你没的2', 'reques': '大爷的2'}}}

总结:Thread通常所谓本地的变量,主要是为每个线程(也可以是协程)内部创建一个字典,字典内部再存储我们的需要定义属于线程自己的内部的变量信息。

个人其他博客地址

简书:https://www.jianshu.com/u/d6960089b087

掘金:https://juejin.cn/user/2963939079225608

小钟同学 | 文 【原创】| QQ:308711822

  • 1:本文相关描述主要是个人的认知和见解,如有不当之处,还望各位大佬指正。
  • 2:关于文章内容,有部分内容参考自互联网整理,如有链接会声明标注;如没有及时标注备注的链接的,如有侵权请联系,我会立即删除处理哟。




推荐阅读
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • 怀疑是每次都在新建文件,具体代码如下 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 使用正则表达式爬取36Kr网站首页新闻的操作步骤和代码示例
    本文介绍了使用正则表达式来爬取36Kr网站首页所有新闻的操作步骤和代码示例。通过访问网站、查找关键词、编写代码等步骤,可以获取到网站首页的新闻数据。代码示例使用Python编写,并使用正则表达式来提取所需的数据。详细的操作步骤和代码示例可以参考本文内容。 ... [详细]
  • Win10 64位旗舰版的优势及特点详解
    本文详细介绍了Win10 64位旗舰版的优势及特点,包括更安全的源安装盘、永久激活方式、稳定性和硬件驱动的集成,以及人性化的维护工具和分区功能。通过阅读本文,您将了解到Win10 64位旗舰版相比其他版本的优势和特点。 ... [详细]
author-avatar
king1994
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有