目录
2.1、ContextMeta,ContextVarMeta和TokenMeta三个元类
前言
- 在Python3.7后官方库出现了contextvars模块, 它的主要功能就是可以为多线程以及asyncio生态添加上下文功能,即使程序在多个协程并发运行的情况下,也能调用到程序的上下文变量, 从而使程序逻辑解耦。
- 在flask框架中,flask1.x版本使用了LocalStack来管理上下文,LocalStack是基于Local即一个字典来实现的上下文管理,在flask2.x版本以后使用了ContextVar管理上下文,那么ContextVar实现上文管理的原理是什么?带个这个问题,分别从ContextVar的使用和源码去看看它是如何完成上下文管理的吧
【注:下面所有Python代码都是基于Python3.9版本的】
【注:如果对异步函数定义和使用不清楚可见】Python异步编程之协程-CSDN博客
1、contextvar的使用
1.1、在普通函数中的使用
1、contextvar的基本使用,包括set()设置值、get()获取值、reset()重置值、获取old_value值、默认值设置
from contextvars import ContextVar
ctx = ContextVar("debug test")
# 设置默认值的方式
# ctx = ContextVar("debug test", default="high")
# 给ctx设置第一个值
token1 = ctx.set("hello")
# 给ctx设置第二个值
token2 = ctx.set("world")
# 获取当前ctx的最新值
print(ctx.get()) # world
# 获取token2对象的旧值
print(token2.old_value) # hello
# 重置token2,此时ctx的当前值变为旧值 hello
ctx.reset(token2)
print(ctx.get()) # hello
# 获取token1的旧值,因为token1没有旧值,所以会返回默认设置对象<Token.MISSING>
print(token1.old_value) # <Token.MISSING>
# 再次把token1的值重置,此时如果ContextVar()初始化没有设置默认值,get()函数返回也没有设置默认值,则会抛出LookupError异常
# 如果存在默认值优先返回get()函数设置的默认值,其次在是ContextVar()初始化设置的默认值
ctx.reset(token1)
print(ctx.get()) # 此处抛出LookupError异常
# get()函数设置默认值方式
# print(ctx.get("tom")) # 此处返回tom
2、run()函数的使用,用于执行一个可调用对象时隔离当前上下文;contextvars.Context 它负责保存 ContextVars 对象和设置的值之间的映射(保存上下文实际是Context完成的),但实际使用中不会直接通过 contextvars.Context 来创建保存上下文,而是通过 contentvars.copy_context 函数来创建。
import contextvars
ctx1 = contextvars.ContextVar("ctx1")
ctx2 = contextvars.ContextVar("ctx2")
ctx1.set("hello")
ctx2.set("world")
# 复制一个当前的上下文对象(注意这是一个带有当前上下文值的新对象)
context = contextvars.copy_context()
# 遍历得到当前上下文所有的ContextVar对象
for ctx, value in context.items():
print(ctx.get(), ctx.name, value)
"""
输出结果:
world ctx2 world
hello ctx1 hello
"""
def task():
print(f"befor task: {ctx1.get()} context[ctx1]: {context[ctx1]}")
ctx1.set("task")
print(f"after task: {ctx1.get()} context[ctx1]: {context[ctx1]}")
"""
输出结果:
befor task: hello context[ctx1]: hello
after task: task context[ctx1]: task
"""
context.run(task)
print(f"ctx1: {ctx1.get()} context[ctx1]: {context[ctx1]}") # ctx1: hello context[ctx1]: task
- 注意:为什么context[ctx1]在task函数中由hello改变成task,出了task()函数context[ctx1]还是task而不是hello。不应该和ctx1一样保持上下文一致吗?
- ctx1保持上下文一致是因为在task()函数内和在task()函数外使用的上下文不是同一个上下文所以才会保持一致;
- 再看context[ctx1]对象不管是在task()函数内还是在task()函数外context对象自始至终都是同一个,所以context对象中ctx1的值在task()函数内发生了变化,在task()函数外同样也会跟着变化(因为context对象在task()内外指向的都是同一块内存地址)
1.2、在异步函数中的使用
1、异步函数中的使用
- 在异步函数中是如何实现协程上下文切换呢?我们知道在 asyncio 中,Task 是用于封装协程的对象,它代表一个可以被调度和执行的异步任务;其作用有:
- a.封装协程:Task 对象可以封装一个协程函数或可迭代对象(如生成器),使其变成可调度的异步任务。通过 asyncio.create_task() 或 loop.create_task() 方法创建 Task 对象。
- b.异步任务的调度和执行:Task 对象可以被添加到事件循环中,由事件循环调度和执行。事件循环会在适当的时机切换任务,以实现异步执行。一旦 Task 对象被添加到事件循环中,它就可以与其他任务共享 CPU 时间,实现并发执行。
- c. 取消和异常处理:Task 对象可以被取消,即停止其执行。可以使用 Task.cancel() 方法取消任务,并通过捕获 asyncio.CancelledError 异常来处理取消操作。此外,Task 对象还提供了 add_done_callback() 方法,用于注册回调函数以处理任务执行完毕或抛出异常的情况。
- d. 获取任务状态和结果:通过 Task 对象可以获取任务的当前状态,包括已完成、正在运行、已取消等。使用 Task.done() 方法可以检查任务是否已完成,而 Task.result() 方法可以获取任务的返回值(如果有)或引发的异常(如果有)。
Task初始化源码如下:
class Task(futures._PyFuture): # Inherit Python Task implementation
# from a Python Future implementation.
_log_destroy_pending = True
def __init__(self, coro, *, loop=None, name=None):
super().__init__(loop=loop)
if self._source_traceback:
del self._source_traceback[-1]
if not coroutines.iscoroutine(coro):
self._log_destroy_pending = False
raise TypeError(f"a coroutine was expected, got {coro!r}")
if name is None:
self._name = f'Task-{_task_name_counter()}'
else:
self._name = str(name)
self._must_cancel = False
self._fut_waiter = None
self._coro = coro
self._context = contextvars.copy_context()
self._loop.call_soon(self.__step, context=self._context)
_register_task(self)
- 从第19行代码可以看到Task中上下文对象是根据contextvars.copy_context()来实现的,这个过程和上面run()函数的使用原理一样,所以我们知道在异步函数中实现上下文管理是通过contextvars.copy_context()来实现的(注:不同Python版本asyncio实现上下文方式可能不一样。contextvars模块是在Python3.7引入的;asyncio 是在 Python 3.4 版本引入的标准库)。
- 下面是一个示例说明contextvars在异步函数中的使用:
import asyncio
import contextvars
# 创建一个ContextVar
ctx = contextvars.ContextVar("ctx")
ctx.set("hello")
async def task(value, delay):
print("task: ", ctx.get())
ctx.set(value)
await asyncio.sleep(delay)
print("task: ", ctx.get())
async def main():
# 异步执行函数
await asyncio.gather(task('task1', 1), task('task2', 2))
print("main: ", ctx.get())
asyncio.run(main())
# 执行结果
task: hello
task: hello
task: task1
task: task2
main: hello
- 执行过程:
- 输出结果中的前两个“task: hello”是同时打印出来的,
- 然后先打印出:“task: task1”,在打印出:“task: task2”,
- 最后打印出:“main: hello”。
- 结论:从上面的执行过程来看异步执行的task('task1', 1),和task('task2', 2)在ctx.set(value)之后内部的上下文是隔离的,执行完成之后又恢复到了之前的上下文。所以在异步函数中使用contextvars模块管理上下文非常方便。
2、contextvar使用的浅拷贝问题
- contextvars.copy_context()的作用是拷贝一份当前的上下文对象,注意这里拷贝是浅拷贝,也就是当前上下文的一个引用,只有在发生写数据的时候才会进行深拷贝生成一个新的上下文对象。这里和Linux中的进程fork运用了同一种技术:写时拷贝;这就是为什么我会把上面示例的“在ctx.set(value)之后”标注为红色。下面看一个示例:
import asyncio
import contextvars
ctx1 = contextvars.ContextVar('ctx1')
ctx2 = contextvars.ContextVar("ctx2")
ctx1.set("hello")
ctx2.set({"name": "张三"})
print("befroe ctx1_value_addr: ", id(ctx1.get())) # befroe ctx1_value_addr: 2070012128240
print("befroe ctx2_value_addr: ", id(ctx2.get())) # befroe ctx2_value_addr: 2070012049728
async def main():
ctx1.set(1) # 此时会进行写时拷贝
print("main ctx1_value_addr: ", id(ctx1.get())) # main ctx1_value_addr: 2069998823728
ctx2_dict = ctx2.get() # 此处只是引用即浅拷贝
print("main ctx2_value_addr: ", id(ctx2_dict)) # main ctx2_value_addr: 2070012049728
ctx2_dict["name"] = "李四"
print(f"main inner ctx1: {ctx1.get()} ctx2: {ctx2.get()}") # main inner ctx1: 1 ctx2: {'name': '李四'}
print(f"before run ctx1: {ctx1.get()} ctx2: {ctx2.get()}") # before run ctx1: hello ctx2: {'name': '张三'}
asyncio.run(main())
print(f"after run ctx1: {ctx1.get()} ctx2: {ctx2.get()}") # after run ctx1: hello ctx2: {'name': '李四'}
# 执行结果
befroe ctx1_value_addr: 2070012128240
befroe ctx2_value_addr: 2070012049728
before run ctx1: hello ctx2: {'name': '张三'}
main ctx1_value_addr: 2069998823728
main ctx2_value_addr: 2070012049728
main inner ctx1: 1 ctx2: {'name': '李四'}
after run ctx1: hello ctx2: {'name': '李四'}
- 首先我们分析打印的ctx值,在before run打印的ctx的值和after run的对比发现ctx2的值发生了变化,乍一看显然有点不符合异步执行切换上下文的情况啊,
- 然后我们再进入到main()的内部看看ctx1和ctx2有什么差别导致出现这样的结果;发现ctx1使用了set()函数也就是重新设置了值,ctx2只使用了get()函数没有重新设置值,结合上面所说的contextvars.copy_context()的作用是拷贝一份当前的上下文对象,只是进行浅拷贝,貌似可以解释这个问题,为了验证实际是不是真是这样,
- 我们分别在main()外和main()内打印出了ctx1值和ctx2值的地址,发现ctx1值前后的地址发生了改变,ctx2值前后的地址并没有发生变化,正好就对应上面所说的浅拷贝情况。
2、contextvar源码分析
查看contextvar源码的方式
- 1、在gitlab上查看:https://github.com/MagicStack/contextvars/blob/master/tests/init.py
- 2、下载contextvars查看源码:pip install contextvars
2.1、ContextMeta,ContextVarMeta和TokenMeta三个元类
- 三个元类的功能:防止用户来继承Context,ContextVar和Token
- 实现原理:只有 contextvars 模块中的 Context 类才能被用作基类,如果其他类尝试继承自 Context,则会触发 TypeError。
- 查看ContextMeta元类的源码如下:
class ContextMeta(type(collections.abc.Mapping)):
# contextvars.Context is not subclassable.
def __new__(mcls, names, bases, dct):
cls = super().__new__(mcls, names, bases, dct)
# 只有 contextvars 模块中的 Context 类才能被用作基类,如果其他类尝试继承自 Context,则会触发 TypeError。
if cls.__module__ != 'contextvars' or cls.__name__ != 'Context':
raise TypeError("type 'Context' is not an acceptable base type")
return cls
2.2、contextvar基本的上下文管理方式
- 从下面源码可以看到contextvar是基于threading.local()生成自己的全局context,从源码中可以看出_state是threading.local()的引用;我们知道线程的上下文管理可以基于threading.local()实现,作为基于线程实现的协不能直接基于threading.local()实现上下文管理;所以contextvar模块提供了支持协程的上下文管理;以下源码是contextvar模块基本的上下文管理方式,实现复杂的线程、协程的上文管理就是基于此部分:
# 复制上下文
def copy_context():
# 复制当前上下文返回一个新的上下文
return _get_context().copy()
# 获取上下文
def _get_context():
# 从_state中获取context对象
ctx = getattr(_state, 'context', None)
if ctx is None:
# 如果ctx对象为空,则使用Context初始化一个ctx对象
ctx = Context()
# 把ctx对象赋值给_state.context
_state.context = ctx
# 返回ctx
return ctx
# 设置上下文
def _set_context(ctx):
_state.context = ctx
# 定义全局context使用threading.local()实现,用于线程、协程的上下文管理
_state = threading.local()
2.3、contextvar之Context实现
- Context作用:创建上下文对象,存储上下文,完成上下文对象切换
- Context源码实现如下:
class Context(collections.abc.Mapping, metaclass=ContextMeta):
def __init__(self):
# 初始化一个不可变字典对象,用于保存上下文
self._data = immutables.Map()
# 初始化一个_prev_context对象,用于保存之前的上下文
self._prev_context = None
# 用于执行一个可执行对象,并不影响当前上下文
def run(self, callable, *args, **kwargs):
if self._prev_context is not None:
raise RuntimeError(
'cannot enter context: {} is already entered'.format(self))
# 获取当前的上下文对象并保存到_prev_context对象中
self._prev_context = _get_context()
try:
# 设置上下文为当前对象的上下文,到切换上下文的作用
_set_context(self)
# 执行可调用对象
return callable(*args, **kwargs)
finally:
# 执行完成可调用对象之后,切换到之前的上下文
_set_context(self._prev_context)
self._prev_context = None
# 复制上下文
def copy(self):
# 创建一个新的上下文对象
new = Context()
# 把当前上下文的值赋值给新的上下文对象
new._data = self._data
# 返回一个新的上下文对象
return new
- 1、其中使用了immutables.Map()创建了一个不可变的字典对象赋值self._data对象,也就是每次对self._data新增值都会返回一个新的对象,并不会影响到原来的对象;目的是:防止调用copy方法后得到的上下文的变动会影响到了原本的上下文变量。下面是对immutables.Map()使用的示例:
data = immutables.Map()
data1 = data.set("name", "张三")
data2 = data1.set("age", "18")
print(data1)
print(data2)
# 输出结果
immutables.Map({'name': '张三'})
immutables.Map({'name': '张三', 'age': '18'})
- 2、run()函数的使用:生成一个新的上下文变量给另外一个线程使用(可调用对象), 并且这个新的上下文变量跟原来的上下文变量是一致的。
- 执行run的时候需要copy一个新的上下文来调用传入的函数;根据immutables.Map的属性, 函数中对上下文的修改并不会影响旧的上下文变量,达到进程复制数据时的写时复制的目的,这样可以降低内存的开销提高程序的执行效率。
- 函数执行完了会再次切换到旧的上下文, 从而完成一次上下文切换.
run()使用示例如下:
from contextvars import ContextVar
ctx_g = ContextVar("test")
ctx_g.set({"name": "张三"})
def main():
new = ctx_g.set({"name": "李四"})
print(f"main: old_value: {new.old_value}; new_value: {ctx_g.get()}")
ctx = contextvars.copy_context()
ctx.run(main)
print("ctx_g", ctx_g.get())
# 输出结果
main: old_value: {'name': '张三'}; new_value: {'name': '李四'}
ctx_g {'name': '张三'}
2.4、Contextvar之Token的实现
- 作用:上下文的本质是一个栈, 每次set一次对象就向栈增加一层数据, 每次reset就是pop掉顶层的数据, 而在Contextvars中, 通过Token对象来维护栈数据之间的交互和ContextVar配合使用。
- 源码如下:
class Token(metaclass=TokenMeta):
MISSING = object()
def __init__(self, context, var, old_value):
# 存储当前上下文对象
self._context = context
# 储存当前set的数据
self._var = var
# 存储上次set的数据
self._old_value = old_value
# 当前token对象是否使用表示
self._used = False
@property
def var(self):
return self._var
@property
def old_value(self):
return self._old_value
def __repr__(self):
r = '<Token '
if self._used:
r += ' used'
r += ' var={!r} at {:0x}>'.format(self._var, id(self))
return r
2.5、Contextvar之ContextVar的实现
- ContextVar在实际使用中使用最多的类,它提供了set(设置值)、get(获取值)、reset(重置值)三个方法,通过Context类实现值的写入获取,通过Token类实现set和reset。
- 1、初始化ContextVar对象:
class ContextVar(metaclass=ContextVarMeta):
def __init__(self, name, *, default=_NO_DEFAULT):
if not isinstance(name, str):
raise TypeError("context variable name must be a str")
# 用于调试
self._name = name
# 给当前对象设置默认值
self._default = default
- 2、set() 为当前上下文设置变量,返回带有旧值的Token对象
def set(self, value):
# 获取当前上下文对象"Context"
ctx = _get_context()
# 临时变量保存当前上下文的_data值
data = ctx._data
try:
# 获取Context的旧对象
old_value = data[self]
except KeyError:
# 获取不到就赋值为object(全局唯一)
old_value = Token.MISSING
# 设置的新的值
updated_data = data.set(self, value)
# 更新当前上下文的_data
ctx._data = updated_data
# 返回带有旧值的token
return Token(ctx, self, old_value)
- 3、get() 从当前上下文获取变量
def get(self, default=_NO_DEFAULT):
# 获取当前上下文"Context"
ctx = _get_context()
try:
# 返回当前上下文的值
return ctx[self]
except KeyError:
pass
# 返回调用get时设置的默认值
if default is not _NO_DEFAULT:
return default
# 返回初始化ContextVar时设置的默认值
if self._default is not _NO_DEFAULT:
return self._default
# 如果没有返回值则抛出异常
raise LookupError
- 4、reset() 清除当前上下文数据
def reset(self, token):
# token是已被使用
if token._used:
raise RuntimeError("Token has already been used once")
# token是否是当前contextvar对象
if token._var is not self:
raise ValueError(
"Token was created by a different ContextVar")
# token中的上下文是否和当前上下文一直
if token._context is not _get_context():
raise ValueError(
"Token was created in a different Context")
# 获取token中上下文对象
ctx = token._context
# 如果没有旧值直接删除该值
if token._old_value is Token.MISSING:
ctx._data = ctx._data.delete(token._var)
else:
# 如果存在旧值就把当前上下文的值设置旧值
ctx._data = ctx._data.set(token._var, token._old_value)
# 标记此token已被使用
token._used = True