什么是上下文
所谓上下文,可以认为是一个程序运行时所需要的环境,比如一些变量,数据的保存。
比如当你的同事在开培训会的时候,你迟到了,如果别人已经讲了半天,你半路插进来,你可能接下来的时间就完全听不懂别人在讲什么了,因为你缺少了上下文环境,这就叫脱离上下文。
在flask框架中也是这样,将程序运行所需要的数据封装到一个对象中,以便你在程序中各个位置都可以调用。
如何使用上下文
为了能让你再程序的各个位置都能随时调用这些数据,有两种做法
- 直接把这个对象当做参数传入到你需要执行的函数中,做成局部变量,比如django框架是这种做法,这样做避免了多线程访问时数据错乱问题,但是很麻烦啊。
def view1(request):
request.xxx # 调用request对象
return 'response'
def view2(request):
request.xxx # 调用request对象
return 'response'
- 做成全局变量,你使用起来会更随意,但是会带来一个问题,当多个线程或协程都来修改这些全局变量时,因为线程是抢占式的,而且进程下的线程间共享全局变量那么如何保证线程安全??
from flask import request
@app.route('/')
def view1():
request.xxx # 调用request对象
return 'response'
多线程修改全局变量造成数据被其他线程篡改
以下是一个用户存钱的案例,有一个存钱的函数,
每个线程过来会调用这个函数,将本线程存的钱数当做参数传给存钱函数,
函数会打印出每个用户存多少钱
单线程调用时不会产生任何问题,你传入多少前就存入多少钱
多线程修改这个全局对象时会产生意想不到的情况,当然你会想到加锁,加锁由并发变成了串行,势必造成效率减慢,这不是框架所追求的。
from threading import Thread
import time
class Request:
def __init__(self):
self.money = None
request = Request()
def save_money(i):
request.money = i
time.sleep(0.001) # 因为线程运行非常快,可以尝试修改睡眠时间观察money的值如何变化
print('我是用户%d' % i, '我存入了%d元' % request.money)
# 单线程调用
# save_money(2)
# 多线程调用
for i in range(20):
t = Thread(target=save_money, args=(i,))
t.start()
如何将各个线程数据隔离开来
我们都有这样的经历:当有很多人给你布置任务的时候,你很有可能非常忙乱,要么是遗漏了某个任务,要么是将某两个人的任务弄混了,如果此时你有一支笔,一张纸,你记录了一张名单,需要的时候查一下这张名单,是不是就不会乱了?
那么此时我们的想法是这样的:
每个线程不是有一个线程标识嘛,那么我利用这个线程标识维护一个字典,把线程标识当做key,把需要保存的数据当做value,不就可以了吗?
我们来实现一个类,这个类叫做仓库,仓库有一个个编号,每个编号内装着对应数据
代码实现:
from threading import Thread
import time
from _thread import get_ident
class Local:
def __init__(self):
object.__setattr__(self, 'ident', get_ident)
object.__setattr__(self, 'storage', {})
def __setattr__(self, key, value):
ident = self.ident()
data = self.storage.get(ident)
if not data:
data = {key: value}
else:
data[key] = value
self.storage[ident] = data
def __getattr__(self, item):
ident = self.ident()
data = self.storage.get(ident)
if not data:
return None
return data.get(item, None)
# class Request:
# def __init__(self):
# self.money = None
# request = Request()
request = Local()
def save_money(i):
request.money = i
time.sleep(0.001) # 因为线程运行非常快,可以尝试修改睡眠时间观察money的值如何变化
print('我是用户%d' % i, '我存入了%d元' % request.money)
# 单线程调用
# save_money(2)
# 多线程调用
for i in range(20):
t = Thread(target=save_money, args=(i,))
t.start()
flask请求上下文和应用上下文
现在可以保证request这个全局变量的线程安全了,但是flask之提供了这一个全局变量吗?其实不是,有以下几个:request, session, g, session, current_app,如果你使用了其他flask插件,他们也会相应的提供一些全局对象,比如flask_login中的current_user
flask会再进行一些封装,将这些全局对象封装到上下文对象中
- 请求上下文封装:
- app
- request
- session
- 应用上下文封装
- app
- g
大家想一下,我们使用request对象时一般在哪个位置调用,是不是都是在视图函数中?我们在视图函数中使用request对象时考虑过线程安全问题吗?如果没有的话,那么肯定是flask在视图函数分发之前将这个request对象隔离起来了,接下来思考一下如何放置
刚才我们已经把仓库建好了,我觉得我现在需要创建一个仓库管理员类,这个类负责帮我在请求到来时把上下文放到仓库,请求结束时帮我把上下文销毁掉。类似这样:
flask请求流程
还记得上次我们找到的flask程序运行入口吗?我把flask核心代码拿出来,就下面这十几行
ctx = self.request_context(environ)
error = None
try:
try:
ctx.push()
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except:
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
什么情况下需要使用上下文
flask上下文的仓库存储的其实是一个栈结构,上面我为了讲明白把他简化了,因为flask其实是可以写多个app应用的
经过剖析源码我发现只有在执行视图函数时中flask才会帮你把上下文推入栈,在其他情况下需要你手动推入栈,比如你编写一个离线脚本进行项目测试时
比如我想查看一下app的名字?
from test import app
from flask import current_app
# 比如你想通过这个current_app这个全局对象查看一下当前app的名字
with app.app_context():
print(current_app.name)
又比如你想往数据库中插入一下数据,但是数据库的配置是写在app的config当中的,此时你也需要推一个应用上下文入栈