ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

重入闸与哨兵值:一份试卷的并发与判据概念

重入闸与哨兵值:一份试卷的并发与判据概念 重入闸与哨兵值一份试卷的并发与判据概念标签重入保护、并发安全、哨兵值、超时预算、可重试判据、幂等引言今天这份技术试卷考的不是记没记住某个数字而是几处状态判断的功底同一件事被点了两次该怎么办、同一个 0 在不同的路径下为什么意思正好相反、多个线程同时往一个容器里写会出什么事、一个动作到底该不该允许自动重来一遍。这几道题背后站着两个主题——“并发安全和状态判据”。就像开车最见功力的不是直线加速而是并线、倒车、窄路会车这些状态在变的时刻这篇文章就借这份试卷把这几个概念讲透。一、重入闸同一件事不能被同时做两遍场景一个保存动作有两个地方都能触发它 —— 页面上一个按钮和顺手按下的快捷键。用户手快第一次还没保存完又点了一次或者先点了按钮、转头又按了快捷键 —— 同一个保存被要求做第二次。如果第二次真的从头再做一遍问题就来了同一个动作执行两遍两边各自改同一份数据、互相踩。概念这就是**重入保护re-entrancy guard**要解决的。做法是给这个动作配一道闸门——用一个字段记住当前有没有正在进行的这一次里面没人 → 放新的一次进来顺便把它的凭据存下里面已经有人 → 不放新的直接把同一份凭据还回去。关键在凭据这个概念。这个动作返回的不是结果而是一张正在进行中的凭据在并发编程里就是一个将来会有结果的对象。拿到凭据的人可以等它第二个入口拿到的也是同一张 ——等的是同一次不是各做一遍。打个比方去餐厅点一份餐服务员给你一张取餐号。这张号不代表饭已经好了只代表你那一单正在做。你拿着号回座位等叫号 等凭据这时同伴也来问我们的饭好了吗服务员给的是同一张号—— 因为本来就是同一单不是又下了一份。易错点闸门条件要小心已经做完这个状态。保存一个很小的文件几乎瞬间就完成字段里于是留了一张已经用完的凭据这时若只判凭据是否存在那道闸门就被永久占住了 —— 之后这个动作再也进不来。所以正确的判据是凭据为空或已经完成是或不是且。二、并发安全多个线程抢着用同一个容器试卷里另一道题考一个缓冲用的容器该选哪种类型。场景程序里有好几个干活的后台线程它们都会往同一个日志缓冲区里追加内容另有一个线程负责定期把缓冲区取走、刷到磁盘。也就是说写它的线程和取走它的线程不是同一个写的人还不止一个。概念这就是并发安全thread safety。普通字典假设同一时刻只有一个人动我两个线程同时往里写轻则丢数据重则结构本身被破坏内部链表接成环读它的线程变成死循环。并发版本的容器自带保护写与写之间内部排队读也不会读到改了一半的状态。用法完全一样代价只是多了一点内部协调的开销。打比方普通字典像一本自己用的笔记本随手写随手翻很痛快并发字典像银行的柜台—— 谁来办业务都得先取号排队慢那么一点但绝不会两个人同时往同一个账户上动手脚。所以判据很简单只有确定会被两个以上线程同时碰的容器才用并发版本用完就扔、只在一个线程里流转的普通版本更快。易错点这类问题最坑的是不保证复现 —— 这次跑没事下次偶发一次表现出来就是数据时有时无。这恰恰是最该早点换并发容器、也最该把为什么必须并发写进注释的场景。三、同一个 0两种完全相反的意思试卷里有一道很妙的题一个配置项当它的值是 0 时在读取路径和写入路径下含义正好相反。读取侧读到一个 0意思是还没配置过。这时正确的动作是用默认值放行—— “还没配置不等于配置成了 0”拿它当真值去用整个流程会被一个没人设过的值带偏。写入侧用户把 0 写下去意思是我就是要这个值。这时正确的动作是确认或提醒—— 因为它和没配置长得一模一样一旦存下去以后就再也分不清用户真要 0还是忘了配。概念这里的 0 是一个哨兵值sentinel value—— 一个被借来表示特殊状态的取值。哨兵值的麻烦在于同一个值在不同路径下可以表示完全不同的意思所以必须按这条路径想问什么来判不能只按值是不是 0来判。现实里到处都是这个套路超时填-1表示无限等端口填0表示让系统随便挑一个。打比方门牌号0 号在一个小区里可能指门卫室在另一个小区里可能指尚未分配。同一个数字关键看它在哪一套编号规则里。易错点只要看到用一个特殊值表示未设置就该立刻问一句这是在读还是在写两条路径的处置往往正好相反。四、超时预算给足还是够用就走试卷还考了一个退出流程里的两个超时值 —— 一个给得比较宽一个很短。有没保存的东西时退出先试着把文档存下来或弹个确认让用户决定所以给一个比较宽的上限万一超了记一条日志、提示可能需要人工处理然后照样退。什么都没改时退出只剩下释放资源、关掉窗口这一件事给一个很短的上限就行关不掉也不等。概念这是**超时预算timeout budget**的分配思路 ——超时值不是随手定的是按超了会有什么后果倒推的。关系到用户数据会不会丢的超时要给足只是关个窗口超时就可以很短。打比方像等外卖 —— 关系到饭能不能送到你手上的你愿意多等一会儿只是让骑手顺手关掉楼道灯等不到你直接自己关了。还要记住一条贯穿两处、也最容易被忽略的原则防呆不能把程序变成关不掉。加确认框、加等待本来是为了安全可如果这个等本身没有上限所谓的安全就变成了卡死。所以每一个等都要配一句最多等多久。五、可重试判据什么时候能自愈什么时候必须停试卷里有一道关于自动重试的题问的是一个请求失败时什么条件下允许自动再发一次。最经典的判据其实就藏在 HTTP 里 ——看这个方法是不是幂等的幂等的动作查询、整份覆盖写入、删除—— 同样的请求发一次和发十次结果一样。这种可以放心重试网络抖一下就重发状态不会因此被搞乱非幂等的动作下单、支付、发一条新消息—— 重复发送就是真的重复操作。这种绝不能盲目重试一重试就可能多扣一笔钱、多下一单。概念这是可重试判据的设计 ——“能不能重试不看错误严不严重而看出错那一刻系统的状态还干不干净”。状态没被污染的可以重试重试是幂等的、重放多少次结果一样状态已经被改动、且改到什么程度不清楚的绝不能自动重试。打比方自动售货机没出货你再投一次币重试是合理的但如果它已经卡在正在吐货的半途你继续投币只会让货卡得更乱。配套的一个好习惯是把这类判据单独抽成一个纯逻辑的判断别埋进流程中间。这样它能被单独测试、边界值一条条钉死而不是混在一堆异步调用里只可意会、不可验证。判据和流程分开也是可测试性最基本的功夫。六、总结借这份试卷几处状态判断的功底被串了起来重入闸同一件事不能被同时做两遍 —— 记住进行中的凭据第二次把同一份还回去判据要含已完成否则闸门会被永久占住。并发安全跨线程共用的容器要用并发版本用法一样、只多一点协调开销这类 bug 的特点是不保证复现。哨兵值同一个 0读取侧表示还没配置用默认值、写入侧表示我就要 0要确认—— 判断之前先问这是在读还是在写。超时预算超时值按超了会怎样倒推每一个等待都要配一个上限防呆不能把程序变成关不掉。可重试判据能不能自动重来看出错时状态还干不干净不看错误严不严重判据单独抽成纯逻辑便于测试。一句话收尾工程里真正难的不是写对一次而是在被点两次、被两个线程同时碰、读到还没准备好的值时结果依然是对的。附录这篇里的几个语法点概念讲完了把这几个点落到语法上示意代码名字都泛化了。① 闸门条件为什么用「或」不用「且」// 放行条件里面没有人 if (凭据 空 || 凭据.已完成) 凭据 开始新一次(); // 写成「且」的后果 if (凭据 空 凭据.已完成) // 两个永远不可能同时成立 → 闸门一次都开不了||是短路求值左边为真右边根本不求值。所以凭据 空 || 凭据.已完成是安全的 —— 凭据为空时不会再去它身上取已完成这个属性换成就会先取属性、直接抛空引用。②await一个任务不是调用一个方法任务 甲 开始保存(); // 拿到的不是结果而是正在进行的凭据 任务 乙 开始保存(); // 第二次进来拿到的是同一个凭据 等待 甲; 等待 乙; // 等的是同一次底层不会跑两遍await的语义是等这个任务跑完不是再跑一遍。同一个任务对象被 await 多少次底层的工作只发生一次 —— 这正是重入闸能成立的语法基础。③ 同一个接口两种实现普通容器 vs 并发容器字典键, 值 本子 新建(); // 单线程 并发字典键, 值 柜台 新建并发(); // 跨线程 本子[键] 值; // 存 柜台[键] 值; // 一样的写法泛型参数键, 值决定按什么存、取出来是什么并发版本只是同一个用法的另一个实现换过去不用改调用处。④ 给等配一个上限任务 甲 开始保存(); 任务 乙 延时(上限); if (等待任一完成(甲, 乙) ! 甲) // 先完成的是上限 → 说明超时了 记录(超时未完成也继续);等待任一完成(a, b)只负责谁先好就返回谁它不会去取消那个还没好的任务 —— 超时之后底层工作其实还在跑。想要真取消必须显式发出取消信号不能指望这个等待替你收场。
返回列表