本文最后更新于:2024年7月30日 上午

在编程中,“上下文”这个词通常指的是程序执行的环境和相关状态信息。本文记录相关内容。

定义

上下文(context):位于关键词前部或后部的词句或文字。它是关键词所处的语言环境,影响关键词的含义。通过阅读上下文,有助于理解和鉴别某关键词所具有的准确含义和用法,判断文献是否相关。

—《图书馆·情报与文献学名词》

上下文切换(context switch):根据某种条件,暂停当前进程或线程的执行,保护当前进程或线程的现场,恢复另一个进程或线程的现场,转而执行该进程或线程的过程。

—《计算机科学技术名词 》 (第三版)

分类

编程上下文共有三个类别。

业务上下文(基础实体)

譬如说:某个后台系统,登录实体为个人,所以功能都是以个人为基准来触发的。但有一天,业务需求,集体账号也可以登录,该账号登录后要获取到集体所有个人的信息,那在触发功能的时候,原先以个人为基准触发的功能就需要兼容到集体账号。这个过程就是的业务上下文变更的操作。

业务上下文笼统的说就是业务操作所对应的基础单位(实体),那如果业务上下文需要变更,那需要更改的代码以及需要做的回归测试是很庞杂的。所以业务上下文变更需要慎之又慎。

运行程序上下文

大家应该对一句话有印象:(进程/线程)上下文切换开销很大。这里的上下文一般指的就是运行程序的上下文。

一个应用程序有可能由多个不同的编程语言文件组成。
比如web网页,由html,css,js文件组成,每个编程文件都有自己的上下文,所以要如何将html中的数据传递到js文件中呢?这便是 data-* 元素属性的目的了。

data-* 全局属性 是一类被称为自定义数据属性的属性,它赋予我们在所有 HTML 元素上嵌入自定义数据属性的能力,并可以通过脚本在 HTML 与 DOM 表现之间进行 专有数据的交换
通过添加 data-* 属性,即使是普通的 HTML 元素也能变成相当复杂且强大的编程对象。

又比如原生应用的webview页面时,会处理webview页面和原生应用之间的通信问题。一般地,都是原生应用在window对象上挂载一系列方法,JS代码可以通过调用这些方法从而触发原生应用的功能。该过程通信慢,响应不够快速。所以社区产出了react-native模块来优化通信问题。这里的通信问题就是进程上下文切换开销大的缘故。

方法/类所在的上下文(作用域)

方法上下文可以理解为方法所在的作用域。类亦如此。

还有一些比较特殊的概念实际上也是和方法上下文相关。比如worker语言特性中,需要监听message事件,这实质上,就是在当前上下文监听另一个上下文的状态。再比如回调函数。实际上是因为上下文变更后,对原有上下文访问的一种编程手段。从这个角度看,闭包实质上也是对上下文的一种操作手段:返回一个函数,该函数在当前上下文可以操作闭包上的上下文。

含义

具体来说,上下文可以有以下几个层面的含义:

  1. 代码上下文:指的是代码片段周围的代码以及它们之间的关系。理解代码上下文对于读懂代码、发现和修复bug至关重要。
  2. 执行上下文:当函数或方法被调用时,它会创建一个执行上下文,这个上下文包括了局部变量、参数、返回地址等信息。在JavaScript等语言中,执行上下文还包括this的值。
  3. 线程上下文:在多线程编程中,每个线程都有自己的上下文,包括线程的栈、程序计数器、寄存器状态等。线程上下文切换是指保存当前线程的状态并恢复另一个线程的状态以执行。
  4. 系统上下文:指的是程序运行时的系统环境,包括操作系统、硬件、网络环境、环境变量等。
  5. 应用上下文:在应用程序中,上下文可能指的是应用程序的当前状态,比如用户的登录状态、应用配置、数据库连接等。
  6. 上下文切换:在操作系统中,上下文切换是指处理器从一个任务切换到另一个任务的过程,这个过程涉及到保存当前任务的状态和加载下一个任务的状态。

形象描述

其实我们编写程序,大概都是这个的范式:

  • 触发。即一段逻辑运行的原因。这个原因可以是比如:

    • 用户点了下鼠标;
    • CPU收到了一个网卡来的“数据到达”的中断;
    • 服务器收到一个请求;
    • 定时任务的时间到了;
    • ……
  • 做决策。俗称业务逻辑。比如

    • 用户刚注册,因此应该给他发个50元的券;
    • 一个下单的用户被检测出在黑名单里,根据风控规则直接拒绝;
    • 请求来自老版本的客户端,因此应该按照旧版的response返回保持向前兼容;
    • 请求流量超过阈值,因此应该限流;
    • 内存分配器来了个请求1G内存的请求,当前的内存池子无法容纳,因此决定向操作系统要一大块新内存;
    • 一个数据查询请求在cache查询不到,因此应该回源;
    • ……
  • 执行。比如:

    • 调用扣库存的接口;
    • 把内存分配好;
    • 在界面上画一个转圈的效果,表示功能正在执行;
    • 直接返回错误或者panic;
    • ……

在做决策的步骤中,一个关键的步骤就是根据足够多的信息做决策。所有必要的信息统称为【上下文】。

在实际编码过程中,上下文的存在有很多具体的形式。比如全局变量,ThreadLocal,函数参数等。

如果一段逻辑很简单,函数参数就足够用了。比如

1
2
3
User getUserByID(int userID) {
return repo.Get(userID);
}

这里userID是一个参数,repo是一个全局可访问的代表db的对象。(在Java里,因为不支持真正的全局变量,一般会被注入到当前的Bean里,但这不影响讨论。)

但逻辑复杂之后,这些参数就不一定够用了。比如你有如下的options。

  • User的名字是加密的。但有些场景用不到,可以提供一个开关控制返回的User.Name是否要解密后的结果或者干脆留空。
  • 如果一个User是被软删除的,那么应该返回找不到还是返回删除的数据。(比如功能上需要支持恢复就先得拿到之前被删除的数据再清理删除标记)。
  • 这个调用需要一个单独的超时,1500ms。
  • 这个调用内部实现了缓存,但当前场景是要强读数据库的。
  • User有个参数,为了新功能在新版本里要返回A,但未开启这个功能时要返回B。是否开启这个功能要依靠feature gating的灰度能力。
  • 数据库实现了主从,但当前场景是要强读主库,避免主从不一致带来的影响。
  • 调用后的结果不得写入缓存(比如一个job从头到尾把User一个个读一遍,如果写了Cache只能把Cache塞满,或者真正的有用的Cache给evict了)
  • 该调用是一个整体的逻辑的一部分,如果整体逻辑cancel了,当前逻辑也要cancel。
  • 如果当前的调用是个混沌工程的请求,当前逻辑应该以某个概率报错。
  • 当前服务用的存储在不同的部署里,有条件的用内部数据库(比如Oceanbase);但在第三方部署里可能就是个mysql或者pg。到底应该用哪一套repo代码来读的开关。
  • ……

如果还简单用函数参数列表写,可能就变成这样了:

1
2
3
4
5
6
7
8
9
10
11
12
13
User getUserByID(
int userID, // 获取什么用户
int opUserID, // 哪个用户触发了这次的动作,用于feature gating
String env, // 当前是啥部署环境
boolean needName, // 是否要返回用户名(并解密)
boolean includeDeleted, // 是否返回已经删除的数据
ClientInfo client, // 啥客户端发的请求,版本号和平台信息
boolean disableCache, // 是否禁用cache
boolean enforceUseMaster, // 强制用主库
int failRate, // 错误概率
...) {
// 无比复杂的逻辑
}

这代码就没法看了。

这些条件都写函数参数就会让函数参数列表变的很长,因此有一些办法。比如:

  • 增加可选参数。和业务直接相关的参数比较适合放到参数上。
  • 将参数塞入Threadlocal或者context对象里,隐晦的传入。读主标记,禁用缓存,流量数据,混沌标记等比较适合放到这里。
  • 配置文件,动态配置,比如redis,Nacos或者专门开发的运营系统的数据。适合于分布式系统里开发人员总体控制系统的行为,比如超时的阈值,超时等待的时间和最大重试次数等很适合这种场景。
  • 功能开关,根据当前用户ID选择是否开启新功能。功能灰度,A/B Test等适合用这种方式表达。
  • 将整个逻辑从函数改写为对象。再为这个对象编写一个Builder来便于各种不同参数选项的传入。
  • 如果业务逻辑超级复杂,这种形式最为适合。并且构建一个对象可以将上述所有其他形式的上下文统一的放到对象的属性中,供对象的逻辑代码使用。而且上下文构建起来后,很多相关的逻辑都可以共享这个上下文。

我们常说的DDD,设计领域对象,实际上的意思就是说要利用对象来构造一套上下文,支撑相关的所有业务逻辑。

比方说,有一个复杂的推荐逻辑。用户明确的参数就只有一个关键字,要求返回和这个关键字相关的产品。编写一个对象可以维护:

  • 用户在哪个A/B Test的Bucket里。
  • 这个用户的标签都有啥。
  • 这个用户最近30分钟都搜过什么,频率是多少,会不会是个爬虫或者攻击者?
  • 根据关键字召回几个类别的数据。要召回哪些类别可能是运营动态配置的。
  • 不同类别的加权分都是啥。
  • 当前这个召回的超时时间最大是对少,召回的最大数据量是多少。
  • 聚合和精排的逻辑应该用哪几种,目前的数据最适合用哪一种。
  • 获取数据优先用ES搜还是数仓直接读。
  • ……

为此就可以编写一个XXXSearcher的class来串联整套逻辑。如果逻辑足够复杂,这个class就可以进一步升级为一个微服务或者一个中台。

因此很多人理解的“面向对象”,除去messaging的那一面,我理解还有一方面的实际上就是以更好的形式去组织业务逻辑的上下文,将业务的复杂性表述清楚,最终指导代码的执行。所以对象内搞定业务逻辑上下文;对象间用messaging通讯(或者说用最简单直接的API通讯)。

大部分的业务开发花最多时间的就是在编写代码去收集这些上下文,但往往最不上心的就是好好组织和管理这些上下文。复杂业务的上下文收集起来成本高昂,但很多人却会忽视他们的重要性,不好好写清楚注释和文档,提高他们的复用性。着实可惜。

—— 知乎

参考资料



文章链接:
https://www.zywvvd.com/notes/protocol/context/context/


“觉得不错的话,给点打赏吧 ୧(๑•̀⌄•́๑)૭”

微信二维码

微信支付

支付宝二维码

支付宝支付

上下文
https://www.zywvvd.com/notes/protocol/context/context/
作者
Yiwei Zhang
发布于
2024年7月30日
许可协议