1、Thread Local(本地线程)
从面向对象设计的角度看,对象是保存“状态”的地方。Python 也是如此,一个对象的状态都被保存在对象携带的一个特殊字典中。Thread Local 则是一种特殊的对象,它的“状态”对线程隔离 —— 也就是说每个线程对一个 Thread Local 对象的修改都不会影响其他线程。
1. (local.py)
2. import threading
3. mydata = threading.local()
4. mydata.number = 42
5. print mydata.number
6. log = []
7.
8. def f():
9. mydata.number = 11
10. log.append(mydata.number)
11.
12. thread = threading.Thread(target = f)
13. thread.start()
14. thread.join()
15. print log
16. print mydata.number
17.
18. >python local.py
19. 42
20. [11] #在线程内变成了mydata.number其他的值
21. 42 #但是没有影响到开始设置的值
这种对象的实现原理也非常简单,只要以线程的 ID 来保存多份状态字典即可,就像按照门牌号隔开的一格一格的信箱。这样来说,只要能构造出 Thread Local 对象,就能够让同一个对象在多个线程下做到状态隔离。这个“线程”不一定要是系统线程,也可以是用户代码中的其他调度单元,例如 Greenlet。
Werkzeug的Local
flask和werkzeug结合紧密,werkzeug是flask的wsgi工具集,所以flask中使用的本地线程是werkzeug中实现的(werkzeug.local.Local)。
werkzeug.local.Local和threading.local区别如下:
(1)werkzeug使用了自定义的__storage__保存不同线程下的状态
(2)werkzeug提供了释放本地线程的release_local方法
(3)werkzeug通过get_ident函数来获得线程标识符
为什么werkzeug还自己搞了一套而不直接使用threading.local呢?
在python中,除了线程之外,还有个叫协程的东东,(这里不提进程)。java中貌似是无法实现协程的。而python的协程感觉高大尚的样子,python3.5开始对协程内置支持,而且也有相关开源库greenlet等。
协程是什么?
举个例子,比如一个线程在处理IO时,该线程是处于空闲状态的,等待IO返回。但是此时如果不让我们的线程干等着cpu时间片耗光,有没有其他办法,解决思路就是采用协程处理任务,一个线程中可以运行多个协程,当当前协程去处理IO时,线程可以马上调度其他协程继续运行,而不是干等着不干活。
这么一说,我们知道了协程会复用线程,WSGI不保证每个请求必须由一个线程来处理,如果WSGI服务器不是每个线程派发一个请求,而是每个协程派发一个请求,所以如果使用thread local变量可能会造成请求间数据相互干扰,因为一个线程中存在多个请求。
所以werkzeug给出了自己的解决方案:werkzeug.local模块。
除 Local 外,Werkzeug 还实现了两种数据结构:LocalStack 和 LocalProxy。
LocalStack 是用 Local 实现的栈结构,可以将对象推入、弹出,也可以快速拿到栈顶对象。当然,所有的修改都只在本线程可见。和 Local 一样,LocalStack 也同样实现了支持 release_pool 的接口。
LocalProxy 则是一个典型的代理模式实现,它在构造时接受一个 callable 的参数(比如一个函数),这个参数被调用后的返回值本身应该是一个 Thread Local 对象。对一个 LocalProxy 对象的所有操作,包括属性访问、方法调用(当然方法调用就是属性访问)甚至是二元操作 [6] 都会转发到那个 callable 参数返回的 Thread Local 对象上,因此也不会影响到其他线程。
flask上下文种类
application context | request context | |
current_app(当前激活程序实例) | request(请求对象,封装了http请求内容) | |
g(处理请求时临时存储的对象) | session(用户会话,用于存储请求之间需要‘记住’的字典值) |
这四种上下文的定义都在flask.globals.py中
1. def _lookup_req_object(name):
2. top = _request_ctx_stack.top
3. if top is None:
4. raise RuntimeError(_request_ctx_err_msg)
5. return getattr(top, name)
6.
7.
8. def _lookup_app_object(name):
9. top = _app_ctx_stack.top
10. if top is None:
11. raise RuntimeError(_app_ctx_err_msg)
12. return getattr(top, name)
13.
14.
15. def _find_app():
16. top = _app_ctx_stack.top
17. if top is None:
18. raise RuntimeError(_app_ctx_err_msg)
19. return top.app
20.
21.
22. # context locals
23. _request_ctx_stack = LocalStack()
24. _app_ctx_stack = LocalStack()
25. current_app = LocalProxy(_find_app)
26. request = LocalProxy(partial(_lookup_req_object, 'request'))
27. session = LocalProxy(partial(_lookup_req_object, 'session'))
28. g = LocalProxy(partial(_lookup_app_object, 'g'))
为什么要用上下文
flask从客户端获取到请求时,要让视图函数能访问一些对象,这样才能处理请求。例如请求对象就是一个很好的例子。要让视图函数访问请求对象,一个显而易见的方法就是将其作为参数传入视图函数,不过这回导致程序中的每个视图函数都增加一个参数,为了避免大量可有可无才参数把视图函数弄得一团糟,flask使用上下文临时把某些对象变为全局可访问(只是当前线程的全局可访问)。
请求上下文(request、session)
生命周期
先回顾一个请求上下文的使用例子
1. @app.route('/')
2. def index():
3. 'User-Agent')
4. return'<p>Your broswer is %s<\p>' % user_agent
在视图函数中,可以直接使用request来获取请求对象。这里牵涉到请求上下文的生命周期问题了。
1. def handle_request():
2. print 'handle request'
3. print request.url
4.
5. handle_request() #会收到运行时错误:RuntimeError: working outside of request context
request对象只有在请求上下文的生命周期内才可以访问。离开了请求的生命周期,其上下文环境也就不存在了,自然也无法获取request对象。
那什么是请求上下文的生命周期呢?一般就是在视图函数里,或者请求钩子中,才能使用请求上下文request。flask在接收到来自客户端的请求时,会帮我们构造请求上下文的使用环境,当flask根据url跳到相应的视图函数的时候,自然而然就可以直接使用请求上下文,请求钩子也是同理。
此外,在不同http请求之间得到的request必然也是不一样的,它只包含当前http请求的一些信息,是线程隔离的。
而session不一样,session是可以跨请求的,在不同的http请求之间,session都是同一个,因此,可以借session来存储一些请求之间需要‘记住’的字典值。
请求上下文环境构造(本篇幅有点长,涉及flask在接收http请求后的工作过程)
1. from flask import Flask
2.
3. app = Flask(__name__) #生成app实例
4.
5. @app.route('/')
6. def index():
7. return 'Hello World'
flask是遵循WSGI接口的web框架,因此它会实现一个类似如下形式的函数以供服务器调用:
1. def application(environ, start_response): #一个符合wsgi协议的应用程序写法应该接受2个参数
2. '200 OK', [('Content-Type', 'text/html')]) #environ为http的相关信息,如请求头等 start_response则是响应信息
3. return [b'<h1>Hello, web!</h1>'] #return出来是响应内容
这个application在flask里叫做wsgi_app。服务器框架在接收到http请求的时候,去调用app时,他实际上是用了Flask 的 __call__方法,这点非常重要!!!
因为__call__方法怎么写,决定了你整个流程从哪里开始。
1. class Flask(_PackageBoundObject): #Flask类
2.
3. #中间省略一些代码
4.
5. def __call__(self, environ, start_response): #Flask实例的__call__方法
6. """Shortcut for :attr:`wsgi_app`."""
7. return self.wsgi_app(environ, start_response) #注意他的return,他返回的时候,实际上是调用了wsgi_app这个功能
如此一来,我们便知道,当http请求从server发送过来的时候,他会启动__call__功能,最终实际是调用了wsgi_app功能并传入environ和start_response。
接下来查看flask中符合wsgi接口的这么一个函数wsgi_app:
1. class Flask(_PackageBoundObject):
2.
3. #中间省略一些代码
4. def wsgi_app(self, environ, start_response):
5. self.request_context(environ)
6. ctx.push()
7. None
8. try:
9. try:
10. self.full_dispatch_request() #full_dispatch_request起到了预处理和错误处理以及分发请求的作用
11. except Exception as e:
12. error = e
13. self.make_response(self.handle_exception(e)) #如果有错误发生,则生成错误响应
14. return response(environ, start_response) #如果没有错误发生,则正常响应请求,返回响应内容
15. finally:
16. if self.should_ignore_error(error):
17. None
18. ctx.auto_pop(error)
首先注意到这两行
1. ctx = self.request_context(environ)
2. ctx.push()
进去request_context方法看看:
1. class Flask(_PackageBoundObject):
2. def request_context(self, environ):
3. return RequestContext(self, environ)
返回了一个RequestContext实例,于是查看类RequestContext的定义:
1. class RequestContext(object):
2.
3. def __init__(self, app, environ, request=None):
4. self.app = app #app是Flask(__name__)实例
5. if request is None:
6. #request_class实质上是flask.wrappers下的class Request。这里根据environ创建一个Request实例
7. self.request = request
8. self.url_adapter = app.create_url_adapter(self.request)
9. self.flashes = None
10. self.session = None
11.
12. self._implicit_app_ctx_stack = []
13. self.preserved = False
14. self._preserved_exc = None
15. self._after_request_functions = []
16.
17. self.match_request()
当第一个http请求过来时,request默认是空的,通过app.request_class(environ)构造
因此ctx获得一个刚初始化过的RequestContext实例,ctx包含了当前请求的request、session等各种信息,非常重要。
然后使用ctx.push()方法,这个方法很重要,继续查看:
1. class RequestContext(object):
2. def push(self):
3. #top其实是个RequestContext实例
4. if top is not None and top.preserved:
5. top.pop(top._preserved_exc)
6. app_ctx = _app_ctx_stack.top
7. if app_ctx is None or app_ctx.app != self.app:
8. self.app.app_context()
9. app_ctx.push()
10. self._implicit_app_ctx_stack.append(app_ctx)
11. else:
12. self._implicit_app_ctx_stack.append(None)
13.
14. if hasattr(sys, 'exc_clear'):
15. sys.exc_clear()
16.
17. self) #将自己入栈
18. self.session = self.app.open_session(self.request) #根据当前客户端的cookie和有效时间来获取保存好的会话
19. if self.session is None: #第一次http请求来时,得到的会话一般是空的
20. self.session = self.app.make_null_session() #创建一个新的会话
先从_request_ctx_stack获取栈顶元素。还记得flask.globals里的代码吗?
1. _request_ctx_stack = LocalStack()
2. _app_ctx_stack = LocalStack()
请求上下文栈和应用上下文栈都是LocalStack数据结构的实例
1. class LocalStack(object):
2.
3. def __init__(self):
4. self._local = Local() #由此可以看出LocalStack实例包裹了一个Local类的实例
5. def push(self, obj):
6. self._local, 'stack', None)
7. if rv is None:
8. self._local.stack = rv = []
9. rv.append(obj)
10. return rv
11.
12. def pop(self):
13. self._local, 'stack', None)
14. if stack is None:
15. return None
16. elif len(stack) == 1:
17. self._local)
18. return stack[-1]
19. else:
20. return stack.pop()
21.
22. @property
23. def top(self):
24. try:
25. return self._local.stack[-1]
26. except (AttributeError, IndexError):
27. return None
28. #LocalStack的push、pop、top的操作对象都是自身的_local对象。
因此,_request_ctx_stack和_app_ctx_stack的push、pop、top操作都是针对自己self._local.stack属性进行的。
而self._local是一个很重要属性,它是WerkZeug.local.Local的实例,是一个本地线程,因此不同线程之间的_request_ctx_stack和_app_ctx_stack是不一样的。
现在回到class RequestContext的push方法,看以下几行代码:
1. class RequestContext(object):
2. def push(self):
3. #省略了一些代码
4. self) #将自己入栈
5. self.session = self.app.open_session(self.request) #根据当前客户端的cookie和有效时间来获取保存好的会话
6. if self.session is None: #第一次http请求来时,得到的会话一般是空的
7. self.session = self.app.make_null_session() #创建一个新的会话
执行ctx.push之后会将ctx自己压入_request_ctx_stack栈中,那么创建的RequestContext实例就被保存到了_request_ctx_stack栈里面了。这里关注另一个问题,请求上下文中session。session最先是根据self.request来打开,这里会根据request的信息,比如当前http请求的cookie,来打开原来就保存好的,跟当前客户端对应的那个会话(不知道这样表述读者能不能理解)。因为要保证不同客户端过来时,打开的会话是不一样的。而同一个客户端的不同请求之间,打开的会话要是一样的。如果打开为空,则表面这个客户端是第一次链接,给它创建一个新的空的会话。
这时候,ctx已经被压栈,同时也已经携带好了request和session两个请求上下文了。但是还差一步,请求上下文的环境才算构造完成。
这时候需要回到globals.py里回看request和session的获得过程:
1. def _lookup_req_object(name):
2. top = _request_ctx_stack.top
3. if top is None:
4. raise RuntimeError(_request_ctx_err_msg)
5. return getattr(top, name)
6.
7.
8. # context locals
9. _request_ctx_stack = LocalStack()
10. _app_ctx_stack = LocalStack()
11.
12. request = LocalProxy(partial(_lookup_req_object, 'request'))
13. session = LocalProxy(partial(_lookup_req_object, 'session'))
request和session都是先从_request_ctx_stack里获取头元素,这个元素是RequestContext实例,它包含了request、session等等的很多信息。request和session就是从它身上获取的。这里可以看到,request和session其实是一个LocalProxy实例,LocalProxy其实就是一个代理,一个为werkzeug的Local对象服务的代理。他把所以作用到自己的操作全部“转发”到 它所代理的对象上去。它初始化的方法如下:
1. @implements_bool
2. class LocalProxy(object):
3.
4. '__local', '__dict__', '__name__', '__wrapped__')
5.
6. def __init__(self, local, name=None):
7. # 这里有一个点需要注意一下,通过了__setattr__方法,self的
8. # "_LocalProxy__local" 属性被设置成了local,你可能会好奇
9. # 这个属性名称为什么这么奇怪,其实这是因为Python不支持真正的
10. # Private member,具体可以参见官方文档:
11. # http://docs.python.org/2/tutorial/classes.html#private-variables-and-class-local-references
12. # 在这里你只要把它当做 self.__local = local 就可以了 :)
13. self, '_LocalProxy__local', local)
14. self, '__name__', name)
15. if callable(local) and not hasattr(local, '__release_local__'):
16. # "local" is a callable that is not an instance of Local or
17. # LocalManager: mark it as a wrapped function.
18. self, '__wrapped__', local)
LocalProxy初始化的时候接收一个可以调用的方法,并且设置为自身的_LocalProxy__local属性。因此request和session本质是一个LocalProxy实例,它们都有一个_LocalProxy__local属性,分别指向偏函数_lookup_req_object(request)和_lookup_req_object(session)。这时候请求上下文的环境就构造完成了。现在相信读者也应该理解了为什么这两个上下文只能在视图函数或者请求钩子里面使用了,因为只有这时候才具备了使用的环境。后面会介绍如何自行构造环境。
请求上下文的使用
既然request和session本质是一个LocalProxy实例,那它是如何使用呢?这里看几个LocalProxy类下的重要的方法:
1. @implements_bool
2. class LocalProxy(object):
3.
4. def _get_current_object(self):
5. """
6. 获取当前被代理的真正对象,一般情况下不会主动调用这个方法,除非你因为
7. 某些性能原因需要获取做这个被代理的真正对象,或者你需要把它用来另外的
8. 地方。
9. """
10. # 这里主要是判断代理的对象是不是一个werkzeug的Local对象,在我们分析request
11. # 的过程中,不会用到这块逻辑。
12. if not hasattr(self.__local, '__release_local__'):
13. # 从LocalProxy(partial(_lookup_req_object, 'request'))看来
14. # 通过调用self.__local()方法,我们得到了 partial(_lookup_req_object, 'request')
15. # 也就是 ``_request_ctx_stack.top.request``
16. return self.__local()
17. try:
18. return getattr(self.__local, self.__name__)
19. except AttributeError:
20. raise RuntimeError('no object bound to %s' % self.__name__)
21. # 接下来就是一大段一段的Python的魔法方法了,Local Proxy重载了(几乎)?所有Python
22. # 内建魔法方法,让所有的关于他自己的operations都指向到了_get_current_object()
23. # 所返回的对象,也就是真正的被代理对象。
24.
25. def __getattr__(self, name):
26. if name == '__members__':
27. return dir(self._get_current_object())
28. return getattr(self._get_current_object(), name)
29.
30. def __setitem__(self, key, value):
31. self._get_current_object()[key] = value
request和session一般的使用如:request.form.get('args_name')、request.args.get('args_name') 或者session['name']=value。
这里分析一个简单的例子:page = request.args.get('page', 1, type=int)
这里会先调用request的__getattr__方法,返回getattr(self._get_current_object(),'args')获取真正的代理对象,即_request_ctx_stack栈顶的RequestContext元素,再在它身上得到request,再获取args。
- <span style="font-size:12px;">class Flask(_PackageBoundObject):
- #中间省略一些代码
- def wsgi_app(self, environ, start_response):
- self.request_context(environ)
- ctx.push()
- None
- try:
- try:
- self.full_dispatch_request() #full_dispatch_request起到了预处理和错误处理以及分发请求的作用
- except Exception as e:
- error = e
- self.make_response(self.handle_exception(e)) #如果有错误发生,则生成错误响应
- return response(environ, start_response) #如果没有错误发生,则正常响应请求,返回响应内容
- finally:
- if self.should_ignore_error(error):
- None
- ctx.auto_pop(error) </span>
在视图函数处理完,wsgi生成完response之后,_request_ctx_stack会将栈顶这个RequestContext实例出栈。虽然ctx已经出栈,但是ctx.session已经通过序列化保存在本地了,但是request已经被抛弃。这就是为什么session可以跨请求使用了。
自行构造请求上下文环境
可以仿照源码的形式,自行构造上下文:
from werkzeug.test import EnvironBuilder
ctx = app.request_context(EnvironBuilder('/','http://localhost/').get_environ())
ctx.push()
try:
print request.url
finally:
ctx.pop()
在flask的官方文档中,它先介绍应用上下文,再介绍请求上下文。在笔者的安排是先介绍请求上下文,再介绍应用上下文。先回忆以下globals.py里关于应用上下文的部分:
- def _lookup_app_object(name):
- top = _app_ctx_stack.top
- if top is None:
- raise RuntimeError(_app_ctx_err_msg)
- return getattr(top, name)
- def _find_app():
- top = _app_ctx_stack.top
- if top is None:
- raise RuntimeError(_app_ctx_err_msg)
- return top.app
- _app_ctx_stack = LocalStack()
- current_app = LocalProxy(_find_app)
- g = LocalProxy(partial(_lookup_app_object, 'g'))
应用上下文(current_app, g)
(1)生命周期
- <span style="font-size:14px;">from flask import Flask, current_app
- app = Flask('SampleApp')
- @app.route('/')
- def index():
- return 'Hello, %s!' % current_app.name</span>
可以通过”current_app.name”来获取当前应用的名称,也就是”SampleApp”。如果还有印象,”current_app”是一个本地代理,它的类型是”werkzeug.local. LocalProxy”,它所代理的即是我们的app对象,也就是说”current_app == LocalProxy(app)”。用”current_app”是因为它也是一个ThreadLocal变量,对它的改动不会影响到其他线程。你可以通过”current_app._get_current_object()”方法来获取app对象。
既然是ThreadLocal对象,那它就只在请求线程内存在,它的生命周期就是在应用上下文里。离开了应用上下文,”current_app”一样无法使用。
- app = Flask('SampleApp')
- print current_app.name
RuntimeError: working outside of application context
其实和request和session这两个请求上下文一样,应用上下文也只能在请求线程内使用。
(2)应用上下文环境构造
由于上篇已经有了介绍,这里直接贴Flask里的wsgi_app方法回顾:
- class Flask(_PackageBoundObject):
- #中间省略一些代码
- def wsgi_app(self, environ, start_response):
- self.request_context(environ)
- ctx.push()
- None
- try:
- #省略一些代码
- finally:
- #省略一些代码
- ctx.auto_pop(error)
- ctx = self.request_context(environ)
- ctx.push()
- class RequestContext(object):
- def push(self):
- #省略一些代码
- app_ctx = _app_ctx_stack.top
- if app_ctx is None or app_ctx.app != self.app:
- self.app.app_context() #创建一个AppContext实例
- app_ctx.push()
- self._implicit_app_ctx_stack.append(app_ctx)
- else:
- self._implicit_app_ctx_stack.append(None)
- #省略一些代码
- class AppContext(object):
- def __init__(self, app):
- self.app = app #app=Flask(__name__)的实例
- self.url_adapter = app.create_url_adapter(None)
- self.g = app.app_ctx_globals_class()
- # Like request context, app contexts can be pushed multiple times
- # but there a basic "refcount" is enough to track them.
- self._refcnt = 0
首先可以看到self.app =app,这个app是app = Flask(__naem__)创建的实例,也就是整个flask app。此外,看到self.g = app.app_ctx_globals_class(),将一个类赋给self.g。通过调试可以看到,self.g是<flask.ctx._AppCtxGlobals object at 0x7ffa7ef366d8>,定位到_AppCtxGlobals查看:
- class _AppCtxGlobals(object):
- """A plain object."""
- def get(self, name, default=None):
- return self.__dict__.get(name, default)
- def pop(self, name, default=_sentinel):
- if default is _sentinel:
- return self.__dict__.pop(name)
- else:
- return self.__dict__.pop(name, default)
- def setdefault(self, name, default=None):
- return self.__dict__.setdefault(name, default)
- def __contains__(self, item):
- return item in self.__dict__
- def __iter__(self):
- return iter(self.__dict__)
- def __repr__(self):
- top = _app_ctx_stack.top
- if top is not None:
- return '<flask.g of %r>' % top.app.name
- return object.__repr__(self)
现在,回到原来RequestContext类下的push方法:
- class RequestContext(object):
- def push(self):
- self.app.app_context() #创建一个AppContext实例
- app_ctx.push()
- self._implicit_app_ctx_stack.append(app_ctx)
- else:
- self._implicit_app_ctx_stack.append(None)
- #省略一些代码
其实和request_ctx.push()效果类似,这里是把app_ctx压入_app_ctx_stack里。
当有了上篇的基础,也可以知道current_app和g都是LocalProxy实例,都有着__local属性,分别指向_find_app()和偏函数_lookup_app_object(g)。
(3)应用上下文的使用
在请求处理结束后:
- class Flask(_PackageBoundObject):
- #中间省略一些代码
- def wsgi_app(self, environ, start_response):
- self.request_context(environ)
- ctx.push()
- None
- try:
- #省略一些代码
- finally:
- #省略一些代码
- ctx.auto_pop(error)
- class RequestContext(object):
- def pop(self, exc=_sentinel):
- self._implicit_app_ctx_stack.pop()
- try:
- #省略一些代码
- finally:
- rv = _request_ctx_stack.pop()
- #省略一些代码
- if app_ctx is not None:
- app_ctx.pop(exc)
从self._implicit_app_ctx_stack取得栈顶元素赋给app_ctx,它同样也是_app_ctx_stack的栈顶元素,app_ctx.pop(exc),从_app_ctx_stack出栈。
就算了解了请求上下文和应用上下文,也会有很多疑惑。
(1)既然请求上下文和应用上下文生命周期都在线程内,其实他们的作用域基本一样,为什么还要两个级别的上下文存在呢?
(2)既然上下文环境只能在一个请求中,而一个请求中似乎也不会创建两个以上的请求或应用上下文。那用ThreadLocal本地变量就行,什么要用栈呢?
(3)为什么要放在“栈”里:在 Web 应用运行时中,一个线程同时只处理一个请求,那么 _req_ctx_stack 和 _app_ctx_stack 肯定都是只有一个栈顶元素的。那么为什么还要用“栈”这种结构?
查了一些资料后,对于第一个问题:虽然在flask应用中,一个app就能基本实现一个简单的web应用,我们知道对一个 Flask App 调用 app.run() 之后,进程就进入阻塞模式并开始监听请求。此时是不可能再让另一个 Flask App 在主线程运行起来的。那么还有哪些场景需要多个 Flask App 共存呢?前面提到了,一个 Flask App 实例就是一个 WSGI Application,那么 WSGI Middleware 是允许使用组合模式的,就可以支持多个app共存。就像request一样,在多app情况下也要保证app之间的隔离。那在flask中如何实现多个app呢?使用中间件DispatcherMiddleware。这个将在以后介绍。(挖坑了。。。)
对于第二第三个问题,其实回答是一样的。在web环境下,确实没必要弄这么麻烦,就算多个 Flask App 同时工作也不是问题,毕竟每个请求被处理的时候是身处不同的 Thread Local 中的。不过Flask支持在离线环境中跑自动测试。但是 Flask App 不一定仅仅在 Web Runtime 中被使用 —— 有两个典型的场景是在非 Web 环境需要访问上下文代码的,一个是离线脚本(前面提到过),另一个是测试。这两个场景即所谓的“Running code outside of a request”。