在Python 3.7中,asyncio 协程加入了对上下文的支持。使用上下文就可以在一些场景下隐式地传递变量,比如数据库连接session等,而不需要在所有方法调用显示地传递这些变量。使用得当的话,可以提高接口的可读性和扩展性。

基本使用方式

协和的上下文是通过 contextvars 中的 ContextVar 对象来管理的。最基本的使用方式是在某一调用层次中设置上下文,然后在后续调用中使用。如下例所示:

import asyncio
import contextvars
from random import randint
from unittest import TestCase request_id_context = contextvars.ContextVar('request-id') async def inner(x):
request_id = request_id_context.get()
if request_id != x:
raise AssertionError('request_id %d from context does NOT equal with parameter x %d' % (request_id, x)) print('start handling inner request-%d, with x: %d' % (request_id, x))
await asyncio.sleep(randint(0, 3))
print('finish handling inner request-%d, with x: %d' % (request_id, x)) async def outer(i):
print('start handling outer request-%d' % i)
request_id_context.set(i)
await inner(i)
print('finish handling outer request-%d with request_id in context %d' % (i, request_id_context.get())) async def dispatcher():
await asyncio.gather(*[
outer(i) for i in range(0, 10)
]) class ContextTest(TestCase): def test(self):
asyncio.run(dispatcher())

上例中,在最后定义了一个单元测试用例对象 ContextTest 。它的方法 test 是程序的入口,使用 asyncio.run 方法来在协程中执行被测试的异步方法 dispatcherdispatcher 则并发启动10个异步方法 outerouter方法首先将在模块层定义的上下文变量 request_id_context 设置为当前调用指定的值,这个值对于每个 outer 的调用都是不同的。 然后在后续被调用的 inner 方法,以及 outer 方法内部访问了这个上下文变更。在 inner 方法内容,则比较了显示传入的 i 和从上下文变量中取出的 request_id

测试用例的执行结果如下:

start handling outer request-0
start handling inner request-0, with x: 0
start handling outer request-1
start handling inner request-1, with x: 1
start handling outer request-2
start handling inner request-2, with x: 2
start handling outer request-3
start handling inner request-3, with x: 3
start handling outer request-4
start handling inner request-4, with x: 4
start handling outer request-5
start handling inner request-5, with x: 5
start handling outer request-6
start handling inner request-6, with x: 6
start handling outer request-7
start handling inner request-7, with x: 7
start handling outer request-8
start handling inner request-8, with x: 8
start handling outer request-9
start handling inner request-9, with x: 9
finish handling inner request-3, with x: 3
finish handling outer request-3 with request_id in context 3
finish handling inner request-7, with x: 7
finish handling outer request-7 with request_id in context 7
finish handling inner request-1, with x: 1
finish handling outer request-1 with request_id in context 1
finish handling inner request-4, with x: 4
finish handling outer request-4 with request_id in context 4
finish handling inner request-5, with x: 5
finish handling outer request-5 with request_id in context 5
finish handling inner request-9, with x: 9
finish handling outer request-9 with request_id in context 9
finish handling inner request-0, with x: 0
finish handling outer request-0 with request_id in context 0
finish handling inner request-2, with x: 2
finish handling outer request-2 with request_id in context 2
finish handling inner request-6, with x: 6
finish handling outer request-6 with request_id in context 6
finish handling inner request-8, with x: 8
finish handling outer request-8 with request_id in context 8

可以看到,虽然每次 outer 方法对模块层同定义的同一个上下文变量 request_id_context 设置了不同的值,但后续并发访问相互之间并不会混淆或冲突。

不同调用层次间对上下文的修改

前一节展示了在设置了上下文变量后,在后续使用中读取这个变量的情况。这一节,我们看一下不用调用层次间对同一个上下文变量进行修改的情况。

在上一节代码上做了一些调整后如下:

import asyncio
import contextvars
from random import randint
from unittest import TestCase request_id_context = contextvars.ContextVar('request-id') obj_context = contextvars.ContextVar('obj') class A(object): def __init__(self, x):
self.x = x def __repr__(self):
return '<A|x: %d>' % self.x async def inner(x):
request_id = request_id_context.get()
if request_id != x:
raise AssertionError('request_id %d from context does NOT equal with parameter x %d' % (request_id, x)) print('start handling inner request-%d, with x: %d' % (request_id, x))
request_id_context.set(request_id * 10)
await asyncio.sleep(randint(0, 3)) obj = A(x)
obj_context.set(obj)
print('finish handling inner request-%d, with x: %d' % (request_id, x)) async def outer(i):
print('start handling outer request-%d with request_id in context %d' % (i, request_id_context.get()))
request_id_context.set(i)
await inner(i)
print('obj: %s in outer request-%d' % (obj_context.get(), i))
print('finish handling outer request-%d with request_id in context %d' % (i, request_id_context.get())) async def dispatcher():
request_id_context.set(-1)
await asyncio.gather(*[
outer(i) for i in range(0, 10)
])
print('finish all coroutines with request_id in context: %d' % (request_id_context.get())) class ContextTest(TestCase): def test(self):
asyncio.run(dispatcher())

具体调整

  1. dispatcher 中,开始启动协程前,将 request_id_context 设置为 -1 。 然后在所有的协程调用完毕后,再查看 request_context_id 的值。
  2. outer 中,在设置 request_id_context 之前,先查看它的值。
  3. inner 中,在检查和查看 request_id_context 之后,将它修改为其原始值的10倍。
  4. 定义了一个对象 A ,以及一个用来传递 A 对象实例的上下文变量 obj_context
  5. inner 中,创建A的实例并保存到obj_context中。
  6. outer中,调用完inner方法后,查看obj_context上下文变量。

代码的执行结果如下:

start handling outer request-0 with request_id in context -1
start handling inner request-0, with x: 0
start handling outer request-1 with request_id in context -1
start handling inner request-1, with x: 1
start handling outer request-2 with request_id in context -1
start handling inner request-2, with x: 2
start handling outer request-3 with request_id in context -1
start handling inner request-3, with x: 3
start handling outer request-4 with request_id in context -1
start handling inner request-4, with x: 4
start handling outer request-5 with request_id in context -1
start handling inner request-5, with x: 5
start handling outer request-6 with request_id in context -1
start handling inner request-6, with x: 6
start handling outer request-7 with request_id in context -1
start handling inner request-7, with x: 7
start handling outer request-8 with request_id in context -1
start handling inner request-8, with x: 8
start handling outer request-9 with request_id in context -1
start handling inner request-9, with x: 9
finish handling inner request-6, with x: 6
obj: <A|x: 6> in outer request-6
finish handling outer request-6 with request_id in context 60
finish handling inner request-0, with x: 0
obj: <A|x: 0> in outer request-0
finish handling outer request-0 with request_id in context 0
finish handling inner request-2, with x: 2
obj: <A|x: 2> in outer request-2
finish handling outer request-2 with request_id in context 20
finish handling inner request-3, with x: 3
obj: <A|x: 3> in outer request-3
finish handling outer request-3 with request_id in context 30
finish handling inner request-5, with x: 5
obj: <A|x: 5> in outer request-5
finish handling outer request-5 with request_id in context 50
finish handling inner request-7, with x: 7
obj: <A|x: 7> in outer request-7
finish handling outer request-7 with request_id in context 70
finish handling inner request-8, with x: 8
obj: <A|x: 8> in outer request-8
finish handling outer request-8 with request_id in context 80
finish handling inner request-9, with x: 9
obj: <A|x: 9> in outer request-9
finish handling outer request-9 with request_id in context 90
finish handling inner request-1, with x: 1
obj: <A|x: 1> in outer request-1
finish handling outer request-1 with request_id in context 10
finish handling inner request-4, with x: 4
obj: <A|x: 4> in outer request-4
finish handling outer request-4 with request_id in context 40
finish all coroutines with request_id in context: -1

观察执行结果,可以看到对上下文变量的修改,有两种情况:

  1. 对于已经设置过值的上下文变量,后续对其做的修改是单向传播的。尽管每个 outer 方法都 request_id_context 设置成了不同的值,但最后在 dispatcher 调用完所有的 outer 后,它取到的 request_id_context 仍然为 -1。 同样,inner方法虽然修改了request_id_context,但这个修改对调用它的outer是不可见的。另外一个方向,outer可以读取到调用它的dispatcher修改的值,inner也可以读取到outer的修改。
  2. 如果是新设置的上下文变量,它的值可以传递到其所在方法的调用者。比如在inner中设置的obj_context,在outer中可以读取。

内存泄漏和上下文清理

根据Python文档, ContextVar对象会持有变量值的强引用,所以如果没有适当清理,会导致内存漏泄。我们使用以下代码演示这种问题。

import asyncio
import contextvars
from unittest import TestCase
import weakref obj_context = contextvars.ContextVar('obj')
obj_ref_dict = {} class A(object): def __init__(self, x):
self.x = x def __repr__(self):
return '<A|x: %d>' % self.x async def inner(x):
obj = A(x)
obj_context.set(obj)
obj_ref_dict[x] = weakref.ref(obj) async def outer(i):
await inner(i)
print('obj: %s in outer request-%d from obj_ref_dict' % (obj_ref_dict[i](), i)) async def dispatcher():
await asyncio.gather(*[
outer(i) for i in range(0, 10)
])
for i in range(0, 10):
print('obj-%d: %s in obj_ref_dict' % (i, obj_ref_dict[i]())) class ContextTest(TestCase): def test(self):
asyncio.run(dispatcher())

和上一节中的代码一样,inner方法在调用栈的最内部设置了上下文变量obj_context。不同的是,在设置上下文的同时,也将保存在上下文中的对象A的实例保存到一个弱引用中,以便后续通过弱引用来检查对象实例是否被回收。

代码的执行结果如下:

obj: <A|x: 0> in outer request-0 from obj_ref_dict
obj: <A|x: 1> in outer request-1 from obj_ref_dict
obj: <A|x: 2> in outer request-2 from obj_ref_dict
obj: <A|x: 3> in outer request-3 from obj_ref_dict
obj: <A|x: 4> in outer request-4 from obj_ref_dict
obj: <A|x: 5> in outer request-5 from obj_ref_dict
obj: <A|x: 6> in outer request-6 from obj_ref_dict
obj: <A|x: 7> in outer request-7 from obj_ref_dict
obj: <A|x: 8> in outer request-8 from obj_ref_dict
obj: <A|x: 9> in outer request-9 from obj_ref_dict
obj-0: <A|x: 0> in obj_ref_dict
obj-1: <A|x: 1> in obj_ref_dict
obj-2: <A|x: 2> in obj_ref_dict
obj-3: <A|x: 3> in obj_ref_dict
obj-4: <A|x: 4> in obj_ref_dict
obj-5: <A|x: 5> in obj_ref_dict
obj-6: <A|x: 6> in obj_ref_dict
obj-7: <A|x: 7> in obj_ref_dict
obj-8: <A|x: 8> in obj_ref_dict
obj-9: <A|x: 9> in obj_ref_dict

可以看到,无论是在outer中,还是在dispatcher中,所有inner方法保存的上下文变量都被没有被回收。所以我们必需在使用完上下文变量后,显示清理上下文,否则会导致内存泄漏。

这里,我们在inner方法的最后,将obj_context设置为None,就可以保证不会因为上下文而导致内存不会被回收:

async def inner(x):
obj = A(x)
obj_context.set(obj)
obj_ref_dict[x] = weakref.ref(obj)
obj_context.set(None)

修改后的代码执行结果如下:

obj: None in outer request-0 from obj_ref_dict
obj: None in outer request-1 from obj_ref_dict
obj: None in outer request-2 from obj_ref_dict
obj: None in outer request-3 from obj_ref_dict
obj: None in outer request-4 from obj_ref_dict
obj: None in outer request-5 from obj_ref_dict
obj: None in outer request-6 from obj_ref_dict
obj: None in outer request-7 from obj_ref_dict
obj: None in outer request-8 from obj_ref_dict
obj: None in outer request-9 from obj_ref_dict
obj-0: None in obj_ref_dict
obj-1: None in obj_ref_dict
obj-2: None in obj_ref_dict
obj-3: None in obj_ref_dict
obj-4: None in obj_ref_dict
obj-5: None in obj_ref_dict
obj-6: None in obj_ref_dict
obj-7: None in obj_ref_dict
obj-8: None in obj_ref_dict
obj-9: None in obj_ref_dict

可以看到,当outerdispatcher尝试通过弱引用来访问曾经保存在上下文中的对象实例时,这些对象都已经被回收了。

总结

在协程中使用 contextvars 模块中的_ContextVar_对象可以让我们方便在协程间保存上下文数据。在使用时要注意以下几点:

  1. contextvars 对协程的支持是从Python 3.7才开始的,使用时要注意Python版本。
  2. ContextVar 应当在模块级别定义和创建,一定不能在闭包中定义。
  3. 保存在上下文中的变量一定要在使用完成后显示清理,否则会导致内存泄漏。

参考资料

Python协程中使用上下文的更多相关文章

  1. 关于python协程中aiorwlock 使用问题

    最近工作中多个项目都开始用asyncio aiohttp aiomysql aioredis ,其实也是更好的用python的协程,但是使用的过程中也是遇到了很多问题,最近遇到的就是 关于aiorwl ...

  2. python 协程(单线程中的异步调用)(转廖雪峰老师python教程)

    协程,又称微线程,纤程.英文名Coroutine. 协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用. 子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在 ...

  3. python 并发专题(十三):asyncio (二) 协程中的多任务

    . 本文目录# 协程中的并发 协程中的嵌套 协程中的状态 gather与wait . 协程中的并发# 协程的并发,和线程一样.举个例子来说,就好像 一个人同时吃三个馒头,咬了第一个馒头一口,就得等这口 ...

  4. python 协程的学习记录

    协程是个子程序,执行过程中,内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行 从句法上看,协程与生成器类似,都是定义体中包含 yield 关键字的函数.可是,在协程中,yield 通常 ...

  5. 00.用 yield 实现 Python 协程

    来源:Python与数据分析 链接: https://mp.weixin.qq.com/s/GrU6C-x4K0WBNPYNJBCrMw 什么是协程 引用官方的说法: 协程是一种用户态的轻量级线程,协 ...

  6. 为何你还不懂得如何使用Python协程

    关于我 一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android.Python.Java和Go,这个也是我们团队的主要技术栈. Github:https:/ ...

  7. Python协程与Go协程的区别二

    写在前面 世界是复杂的,每一种思想都是为了解决某些现实问题而简化成的模型,想解决就得先面对,面对就需要选择角度,角度决定了模型的质量, 喜欢此UP主汤质看本质的哲学科普,其中简洁又不失细节的介绍了人类 ...

  8. python 协程与go协程的区别

    进程.线程和协程 进程的定义: 进程,是计算机中已运行程序的实体.程序本身只是指令.数据及其组织形式的描述,进程才是程序的真正运行实例. 线程的定义: 操作系统能够进行运算调度的最小单位.它被包含在进 ...

  9. Python协程与JavaScript协程的对比

    前言 以前没怎么接触前端对JavaScript 的异步操作不了解,现在有了点了解一查,发现 python 和 JavaScript 的协程发展史简直就是一毛一样! 这里大致做下横向对比和总结,便于对这 ...

随机推荐

  1. 解决Image在canvas以及audio、video在AudioContext下跨域问题

    媒体元素嘛,在对应的标签或对象上加上 crossOrigin = 'anonymous' 即可. 例如 Image 在canvas 上绘制时会跨域: <img crossOrigin=" ...

  2. JavaScript专题

    1. 在ajax的任何回调方法中,比如success回调,使用return,将会无效 2. //todo

  3. p2psearcher绿色版使用方法

    P2pSearcher大家应该很了解,p2p是一款基于P2P技术的资源搜索软件,基于先进的P2P搜索技术,可在瞬间搜遍全球ED2k网络资源,简单便捷的搜索到ED2K网络上共享的海量影音娱乐,学习资料等 ...

  4. JAVA利用jxl读取Excel内容

    JAVA可以利用jxl简单快速的读取文件的内容,但是由于版本限制,只能读取97-03  xls格式的Excel. import java.io.File; import java.io.FileInp ...

  5. JSP实现用户登录样例

    业务描述 用户在login.jsp页面输入用户名密码登录: 如果用户名为xingoo,密码为123,则跳转到成功界面login_success.jsp,并显示用户登录的名字: 如果用户名密码错误,则跳 ...

  6. OpenGL中的数据——Buffer

    OpenGL中主要包括了两种数据——Buffer和Texture. Buffer用于储存线性数无类型据块,可以看成普通的内存块,而Texture则用于储存多维数据,一般储存图像或者其他数据. Buff ...

  7. SAP订单编排和流程增强概述

    SAP产品里的订单处理,无论是On-Premises解决方案还是云产品,我认为归根到底可以概括成四个字:订单编排,包含两个层次的内容: 1. 单个订单通过业务流程或者工作流驱动的状态迁移: 2. 多种 ...

  8. groupadd

    功能说明:用于创建新的用户组. 参数选项:-g gid 指定用户组的gid,除非接-o参数,否则ID值唯一且不为负,如果不指定-g参数,则gid从500开始.-f 新增一个账户,强制覆盖一个已存在的组 ...

  9. 使用jvisualvm.exe工具查看java项目内存溢出(堆溢出)

    在查看内存溢出的时候,我们需要明白,堆溢出和持久代溢出,他们不一样,说到内存泄漏,我们就需要明白,内存中  年老代和新生代,和持久代,这3块的数据 自己的理解: new了一个对象,会进入到堆里面,先放 ...

  10. TSP 模拟退火

    TSP——模拟退火解法 都知道TSP是经典的NP问题,从一个点开始遍历所有点,不重复,求最短路径. 可以用枚举终点,跑流量为2的最小费用,图论来做,时间复杂度为 ​ 费用流已经用到堆优化了.显然点,边 ...