热门标签 | 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:关于文章内容,有部分内容参考自互联网整理,如有链接会声明标注;如没有及时标注备注的链接的,如有侵权请联系,我会立即删除处理哟。




推荐阅读
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社区 版权所有