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
'''
很明显, 数据改乱了, 是个线程更改的都是同一份数据, 对数据产生了不安全性
为了让他们的更改相互隔离, 于是就可以 :
-
加锁 : 不使用该方法, 加锁的思路是多个线程要真正实现共用一个数据, 而我们是要做请求对象的并发, 实现的是该线程对于请求对象这部分内容有任何修改并不影响其他线程
-
使用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()