ContextVars 是 Python3.7 以及之后版本里的标准库。
应用场景
Context managers that have state should use Context Variables instead of threading.local() to prevent their state from bleeding to other code unexpectedly, when used in concurrent code.
从官方文档的说明来看,它是用于解决并发代码里的长生命周期变量的管理问题的。这里的“并发代码”就是指使用了 async/await
的协程代码。
那么,什么是“长生命周期变量”?如果你使用过 bottle/flask 当中的一个就能知道 request
这个变量能在框架处理请求的大部分过程中被调用,并且在不同的请求之间,它的值是互相隔离的。它虽然被创建在整个调用过程的开始,却不需要显式的层层传递给每个函数。它看起来像是一个全局变量,但却不是程序结束才被销毁、并且也不是全局共享的——它对于每个请求来说都是唯一无二的变量。
在传统的多线程编程里,使用 threading.local()
就可以做到这一点,但在 async/await
这种多协程编程里,往往一个线程内要跑许多个协程,使用 threading.local()
无法达到这个目的。但协程又号称微线程,对标的是用户级线程,那么自然也需要一种方式来隔离多个协程之间的长生命周期变量,于是 ContextVars
应运而生。
在官方文档里有很清楚的样例 https://docs.python.org/zh-cn/3/library/contextvars.html#asyncio-support,我就不献丑了,这也不是本文的重点。
实现原理
那么,ContextVars
是怎么做到协程间隔离变量的呢?
看源码是最简单有效的做法,但是一顿追踪溯源之后发现标准库里的 contextvar
是直接嵌入 CPython 源代码里的,在不清楚代码结构的情况下,我看的迷迷糊糊。
这时候我想到 https://github.com/MagicStack/contextvars 是同一个作者为低版本 Python 写的反向实现,并且源码刚好是 Python 的!可以看出来它只是继承了 Mapping
然后实现了 Copy 方法而已,唯一的隔离在 copy_context
里。
既然源码看不出来什么,那么就看看它的相关 PEP 文档——PEP-567。可以看到在 PEP 文档里写的很清楚,在 asyncio 几个关键函数里增加了 copy_context
方法,然后再调用 context.run
去执行回调函数。
简单粗暴但很有效的做法。
这与我们直接使用此标准库提供的接口 context.run
跑普通函数没有区别,asyncio 只不过是把需要程序员手动 copy_context
的行为放到源码里执行了。