Flask自定义local对象

05.Flask自定义local对象

需求与思考

  • 实现并发效果, 每一个请求进来的时候我们都开启一个进程, 这显然是不合理的, 于是就可以使用线程
  • 那么线程中数据互相不隔离,存在修改数据的时候数据不安全的问题。
  • 如果我们的需求是:每个线程都对变量 num 进行设值, 并打印其线程号, 其效果如下 :

一、不用threading.local

# 不用local
from threading import Thread, get_ident  # 可以获取线程id
import time

num = -1


def task(arg):
    global num
    num = arg
    time.sleep(3)
    print(num, f'线程{get_ident()}')


for i in range(10):
    t = Thread(target=task, args=(i,))
    t.start()
'''
9 线程12244
9 线程13324
9 线程7972
9 线程134249 
9 线程12476
9 线程13408
'''

很明显, 数据改乱了, 是个线程更改的都是同一份数据, 对数据产生了不安全性

为了让他们的更改相互隔离, 于是就可以 :

  1. 加锁 : 不使用该方法, 加锁的思路是多个线程要真正实现共用一个数据, 而我们是要做请求对象的并发, 实现的是该线程对于请求对象这部分内容有任何修改并不影响其他线程

  2. 使用threading.local对象把要修改的数据复制一份,使得每个数据互不影响。

    我们要实现的并发是多个请求实现并发,而不是纯粹的只是修改一个数据,所以第二种思路更适合做我们每个请求的并发,把每个请求对象的内容都复制一份让其互相不影响。

详解:为什么不用加锁的思路?加锁的思路是多个线程要真正实现共用一个数据,并且该线程修改了数据之后会影响到其他线程,更适合类似于12306抢票的应用场景,而我们是要做请求对象的并发,想要实现的是该线程对于请求对象这部分内容有任何修改并不影响其他线程。所以使用方案二

二、threading.local对象的使用

  • 注意:local对象只支持线程,不支持协程,生成自己的local对象,各改各的,互不影响
  • 思考:多个线程修改同一个数据,复制多份数据给每个线程用,为每个线程开辟一块空间进行数据存储
from threading import Thread
from threading import local
from threading import get_ident
import time

# local对象,当识别到新的进程会为其开辟一块新的内存空间, 相当于每个线程都对该值进行了拷贝
local_obj = local()
'''
{'线程1':{'value':1},'线程2':{'value':1},'线程3':{'value':1},'线程4':{'value':1}}
'''

def task(arg):
    local_obj.value = arg
    time.sleep(2)
    print(local_obj.value,f'线程号:{get_ident()}')

for i in range(10):
    t = Thread(target=task, args=(i,))
    t.start()
    
"""
2 线程号:11896
3 线程号:8796
9 线程号:3100
8 线程号:9188
....
"""

如上通过threading.local实例化的对象,实现了多线程修改同一个数据,每个线程都复制了一份数据,并且修改的也都是自己的数据。达到了我们想要的效果。

三、通过字典自定义threading.local

面向过程式(函数)

# 自己写一个类似local的东西
from threading import get_ident, Thread
import time

storage = {}


# {'线程id':{value:1},'线程id':{value:2}....}
def set(k, v):
    ident = get_ident()
    if ident in storage:
        storage[ident][k] = v
    else:
        storage[ident] = {k: v}


def get(k):
    ident = get_ident()
    return storage[ident][k]


def task(arg):
    set('val', arg)
    v = get('val')
    print(f'{get_ident()}:{v}')


for i in range(10):
    t = Thread(target=task, args=(i,))
    t.start()

# 10个线程执行 task 的打印结果
'''
4360:0
3816:1
10736:2
1580:3
4772:4
4024:5
9476:6
11036:7
12316:8
7372:9
'''

print(storage)
'''
{
11080: {'val': 0}, 2000: {'val': 1}, 13944: {'val': 2}, 
12552: {'val': 3}, 13736: {'val': 4}, 12476: {'val': 5}, 
9392: {'val': 6}, 14900: {'val': 7}, 12660: {'val': 8}, 8648: {'val': 9}
}
'''

面向对象版

# 面向对象版本
from threading import get_ident, Thread
import time


class Local(object):
    storage = {}

    def set(self, k, v):
        ident = get_ident()
        if ident in Local.storage:
            Local.storage[ident][k] = v
        else:
            Local.storage[ident] = {k: v}

    def get(self, k):
        ident = get_ident()
        return Local.storage[ident][k]


obj = Local()


def task(arg):
    obj.set('val', arg)
    time.sleep(1)
    v = obj.get('val')

    print(f'{get_ident()}:{v}')


for i in range(10):
    t = Thread(target=task, args=(i,))
    t.start()

# 10个线程执行 task 的打印结果
'''
1844:0
7868:1
6040:9
11020:7
6780:8
8988:6
13136:5
14468:4
10892:3
12196:2
'''

print(obj.storage)
'''
{
1844: {'val': 0}, 7868: {'val': 1}, 12196: {'val': 2}, 
10892: {'val': 3}, 14468: {'val': 4}, 13136: {'val': 5}, 
8988: {'val': 6}, 11020: {'val': 7}, 6780: {'val': 8}, 6040: {'val': 9}
}
'''

面向对象方式二:点拦截方法 setattr,getattr实现自定义threthreading.local

from threading import get_ident, Thread
# get_ident()可以获取每个线程的唯一标记
class Local(object):
    storage = {} # 初始化一个字典

    def __setattr__(self, k, v):
        ident = get_ident() # 获取当前线程的唯一标记
        if ident in Local.storage:
            Local.storage[ident][k] = v
        else:
            Local.storage[ident] = {k: v}

    def __getattr__(self, k):
        ident = get_ident()
        return Local.storage[ident][k]

obj = Local()

def task(arg):
    obj.val = arg
    print(obj.val)

for i in range(10):
    t = Thread(target=task, args=(i,))
    t.start()

每个对象有自己的存储空间(字典)

上面已经实现了 local 的功能, 但存在一个问题 : 如果我们想生成多个local对象,但是会导致多个local对象所管理的线程设置的内容都放到了类属性 storage = { }里面, 于是我们可以想到将 storage 设置成对象属性

  • storage = { }设置成对象属性, 实现每一个local对象所对应的线程设置的内容都放到自己的storage里面
from threading import get_ident, Thread

class Local(object):
    def __init__(self):
        object.__setattr__(self, 'storage', {})
        # self.__setattr__('storage', {})  # 该种方式会产生递归调用

    def __setattr__(self, k, v):
        ident = get_ident()
        if ident in self.storage:
            self.storage[ident][k] = v
        else:
            self.storage[ident] = {k: v}

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

obj = Local()

def task(arg):
    obj.val = arg
    print(obj.val)

for i in range(10):
    t = Thread(target=task, args=(i,))
    t.start()

四、使用协程 | 线程来实现请求并发

如果是你会如何设计flask的请求并发?

  • 协程属于应用级别的, 协程会替代操作系统自动切换遇到 IO的任务或者运行级别低的任务, 而应用级别的切换速度远高于操作系统的切换
  • 在flask中为了实现这种并发需求, 依赖于werkzeug包, 我们导入werkzeug下的local查看其源码
try:
    from greenlet import getcurrent as _get_ident  # 获取协程唯一标识
except ImportError:
    from threading import get_ident as _get_ident  # 获取线程唯一标识
  • 发现最开始导入线程和协程的唯一标识的时候统一命名为_get_ident,并且先导入协程模块的时候如果报错说明不支持协程,就会去导入线程的_get_ident,这样无论是只有线程运行还是协程运行都可以获取唯一标识,并且把这个标识的线程或协程需要设置的内容都分类存放于__storage__字典中。
  • 我们使用该方法实现协程 | 线程并发处理请求
try:
    from greenlet import getcurrent as get_ident  # 获取协程唯一标识
except Exception as e:
    from threading import get_ident  # 获取进程唯一标识

from threading import Thread

class Local(object):
    def __init__(self):
        object.__setattr__(self, 'storage', {})

    def __setattr__(self, k, v):
        ident = get_ident()
        if ident in self.storage:
            self.storage[ident][k] = v
        else:
            self.storage[ident] = {k: v}

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

obj = Local()

def task(arg):
    obj.val = arg
    obj.xxx = arg
    print(obj.val)

for i in range(10):
    t = Thread(target=task, args=(i,))
    t.start()
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贾维斯Echo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值