本章导读
卷一到此收官。前面八章打的是"单线程世界的地基":类型与视图(第 3 章)、错误模型(第 4 章)、内存管理(第 5、6 章)、临时区(第 7 章)、版本与裁剪(第 8 章)。本章补上最后一块:多个执行流同时触碰同一份数据时,最底层靠什么不乱。它由三层递进组成:原子操作(xatomic32/64/ptr——单变量的读、写、读改写无交错);自旋锁(xspinlock——几条指令长度的临界区保护);等待原语(int64 与 xwaitresult——超时的数学与等待结果的统一口径)。
这一章是典型的"地基章":第 8 章说过,原子操作的头文件是特性宏编译期分派的首批消费者之一——同一份头文件在无原子指令的平台上走内部实现,在支持的平台上走内联路径,靠的就是这里的特性闭包。往后看,第 53 章的线程同步、第 54 章的取消体系、第 139–140 章配置中心的"原子替换全局指针",全部站在本章原语之上;本章只教原语层——单机、单变量、看得见摸得着的小件,体系化的并发设计留给卷六。读完你应当能回答三个问题:什么场景不需要锁(单变量就够时);什么时候锁也不该太重(自旋与互斥的数量级账);以及"等待一个东西直到超时"为什么必须用绝对截止而不是每轮重设的相对超时。
9.1引入
三段真实事故开路。第一段:计数器丢了更新。 统计服务用 count++ 累计请求数,八个线程各自加了一百万次,总数停在七百二十万附近且每次运行都不同。count++ 是"读—改—写"三步,两个线程的三步可以交错:都读到 41,各加一,都写回 42——一次更新无声消失。Mutex 能修,但为了一个整数的自增付出"睡眠—唤醒"的代价,杀鸡用了牛刀。第二段:标志位说了谎。 生产线程先填好结构体,再用一个标志变量宣布"数据好了";消费线程偶尔看到标志为真,读到的却是半新的数据。没有重排限制的写入之间没有顺序承诺——标志的可见不保证数据的可见。这不是概率玄学,是内存模型问题。第三段:重试循环的总时长失控。 "每轮等 100 毫秒再试,最多试十秒"的代码,实际跑了二十八秒——每一轮都重新等 100 毫秒,等待预算被重置了十几次;系统需要的不是"每次等多久",而是"到什么时候为止"。
三个问题对应三件工具:原子读改写让 count++ 一步完成;内存顺序让"发布—获取"成为可依赖的配对;deadline 数学把超时从相对值变成绝对时刻。它们合起来不到三十个函数,却是全书从队列(第 21 章)到取消令牌(第 54 章)所有并发设施的公共底座。
9.2概念
三种宽度与读改写家族
原子模块提供三种宽度:xatomic32、xatomic64、xatomicptr——32/64 位整数与一个指针宽度。每种宽度的家族形态一致:Init 运行期初始化(静态对象用 XRT_ATOMIC32_INIT(0u) 这类宏,保证所有平台起始状态一致)、Load 读、Store 写、Exchange 整体换、FetchAdd/FetchSub/FetchAnd/FetchOr/FetchXor 读改写、CompareExchange 条件换。"Fetch"前缀的语义统一是返回旧值:xrtAtomic64FetchAdd(&Counter, 1u, ...) 返回加之前的值,计数器本身已经加好——想要新值就用旧值加一,不要回头再 Load 一次(多一次原子操作还多一次时序假设)。
一个直觉性的判断标准:共享的形状决定工具。只是一个计数器、一个标志位、一个"当前指向谁"的指针——单变量,原子足够,无锁且便宜;"两个字段必须一起变"这类不变式,原子做不了(两次原子之间仍然可以被打断),那是锁的地盘(第 53 章);线程之间传的是指针所有权,那是队列的地盘(第 21 章)。层级用错的典型症状:给队列外面包 Mutex(队列本来线程安全)、给计数器上锁(原子便宜一个数量级)。
比较交换:期望值的回写契约
xrtAtomic64CompareExchange(&Counter, &iExpected, 10u, 成功序, 失败序) 是整个家族里最值得单独讲的一个:当前值等于 *iExpected 时换成 10u 返回真;不相等时返回假,并把实际观察到的当前值回写进 *iExpected。这个回写是刻意的契约设计——CAS 循环的标准形态是"拿期望值去试,失败了用回写的实际值修正后重试",不需要再 Load 一次,也不会在"读期望"与"再试"之间引入额外的窗口。失败序因此必须具备获取语义(回写值要被读到),且不能强于成功序、不能包含 RELEASE——这些约束由参数校验兜底,非法组合设置 XERR_ARGUMENT 且对象保持不变。
CAS 的经典用途是"改结构体里的一个字段,但不想锁整个结构体":读出旧值、在本地算出新值、CAS 写回;失败说明有人抢先,用回写值重来。循环的每一步都是原子的,整个"读—算—写"却不是——这正是它的适用边界:冲突稀疏时几乎一次成功,冲突密集时循环本身成为热点。后面的无锁队列(第 21 章)内部就是这个模式的高度工程化。
内存顺序:五个等级与一条默认纪律
每个原子操作都带一个 xmemoryorder 参数:XMEMORY_RELAXED(只保证本次操作原子,不建立任何跨变量顺序——纯计数够用);XMEMORY_ACQUIRE(读取端——本次读之后的访问不许重排到它前面,用于"获取"别人发布的数据);XMEMORY_RELEASE(写入端——本次写之前的访问不许重排到它后面,用于"发布"已写好的数据);XMEMORY_ACQ_REL(读改写两者兼备——CAS 常用);XMEMORY_SEQ_CST(全顺序一致,最直观也最贵)。合法性矩阵很简单:Load 只接受 RELAXED/ACQUIRE/SEQ_CST,Store 只接受 RELAXED/RELEASE/SEQ_CST,违反即 XERR_ARGUMENT。
工程纪律只有一条:没有证明可以放宽之前,用 SEQ_CST。内存序的坑在于"错了也大概率能跑"——重排与可见性问题在特定 CPU、特定编译器优化级别下才显形,测试绿不等于正确。正确的工作流是:先用 SEQ_CST 写对,性能数据(第 136 章的方法)指出某个热点计数器或发布点后,再有依据地降级到 RELAXED 或 ACQUIRE/RELEASE 配对,并在注释里写明为什么安全。平台的承诺是单向的:只提供全栅栏的平台会采用更强的顺序实现,公开契约绝不被弱化——你可以依赖文档里的下限。
"发布—获取"是最常用的一对:生产侧 Store(标志, 1, RELEASE)(此前写的数据全部就绪后才宣布),消费侧 Load(标志, ACQUIRE)(看到标志后读到的数据必然是宣布前的版本)。配对任一侧用了 RELAXED,承诺即告失效——这是本章避坑第一坑的主角。
栅栏、Pause 与无锁判定
三个辅助件补齐家族:xrtAtomicThreadFence(序) 是独立栅栏(不附着于任何变量,给"多个普通写入 + 一个原子标志"这类混合形态兜底);xrtAtomicSignalFence 只约束编译器重排不约束 CPU(信号处理器与线程的边界场景);xrtAtomicPause() 是自旋间隙的提示指令(x86 上是 PAUSE,ARM 上是 YIELD)——告诉超流水线"这个循环在等别人,别把你猜测执行的资源全砸在这里"。xrtAtomicIsLockFree(sizeof(uint64)) 回答"这个宽度在你的平台上是不是真无锁"——绝大多数量级问题在选型阶段问它一次就够了。
自旋锁:几条指令的临界区
xspinlock 建立在原子 CAS 之上,是全库最轻的锁:拿不到就忙等(自旋),不睡眠。它的适用面可以用数量级账说清:临界区只有几条指令时,自旋的代价是一次 CAS 失败加几个 Pause;互斥的代价是潜在的系统调用与线程切换,通常贵一个数量级。反过来,临界区一旦包含 IO、分配、超过几十条指令的计算,自旋就从"便宜"变成"烧 CPU"——忙等的线程不但不干活,还在抢持锁线程的核。所以选型判据是临界区长度,不是"自旋听起来高级"。
生命周期三种形态与第 53 章四件套完全同构:栈上 xrtSpinInit/xrtSpinUnit、静态 XRT_SPIN_INIT、堆上 xrtSpinCreate/xrtSpinDestroy;xrtSpinTryLock 是非阻塞变体(已持有时返回假)。一条硬契约:释放仍被持有的锁失败并设置 XERR_STATE——这不是清理建议,是防泄漏的强制检查。
等待原语:deadline 数学与五态结果
等待接口使用 int64 相对毫秒,XRT_WAIT_FOREVER 表示无限等待。跨步骤总预算可用 xrtTimer() 的 double 秒差计算剩余毫秒;不再定义独立截止时间类型。
xwaitresult 把"等待的结果"拆成五个互斥的值:XWAIT_ERROR(真正失败)、XWAIT_OK(成功)、XWAIT_TIMEOUT(到时)、XWAIT_CANCELLED(被取消——第 54 章的取消令牌会走到这)、XWAIT_CLOSED(等待的对象已关闭)。设计意图是把正常控制流与错误分开:超时与取消是调用方应当处理的预期分支,不是塞进错误链的"异常"。从第 53 章的 xrtThreadWait 到网络库的连接等待,返回的都是这个枚举——本章先把语义记住,体系化的等待与取消在卷六展开。
9.3示例
完整程序一:原子计数与无歧义的 CAS
*/ #include <stdio.h> #include <xrt.h> int main(void) { /* 原子变量必须用 INIT 宏静态初始化,保证所有平台起始状态一致。 */ xatomic64 Counter = XRT_ATOMIC64_INIT(0u); */ uint64 iExpected = 1u; /* 原子自增:返回旧值 0(丢弃),计数器变为 1;纯计数用 RELAXED 即可。 */ (void)xrtAtomic64FetchAdd(&Counter, 1u, XMEMORY_RELAXED); */ if ( xrtAtomic64CompareExchange( &Counter, &iExpected, 10u, XMEMORY_ACQ_REL, XMEMORY_ACQUIRE ) ) { /* 读取最终值 10;没有其他生产者在发布数据,RELAXED 足够。 */ printf("counter=%llu\n", (unsigned long long)xrtAtomic64Load(&Counter, XMEMORY_RELAXED)); } return 0; }
刚才发生了什么。 ① XRT_ATOMIC64_INIT(0u) 宏初始化——静态与栈上对象统一用它,跨平台起始状态一致;② FetchAdd(1, RELAXED) 演示纯计数的最弱序足够——不发布别的数据就不需要顺序承诺;③ CAS 期望值设为 1(与 FetchAdd 后的实际值一致),成功换成 10——成功路径 *iExpected 不动;④ 成功序 ACQ_REL、失败序 ACQUIRE 的搭配是 CAS 的标准姿势:失败序必须能读回写值且不得强于成功序。把 iExpected 改成 2 再运行:CAS 失败,回写把 1 写回去——那个值就是重试的起点。
完整程序二:RMW 全家巡检
*/ #include <stdio.h> #include <stdint.h> #include <xrt.h> int main(void) { xatomic32 A32; xatomic64 A64; xatomicptr APtr; uint32 iOld32 = 0; uint32 iOldAnd = 0; uint32 iOldOr = 0; uint32 iExpected = 0; uint64 iOld64 = 0; ptr pOld = NULL; ptr pExpected = NULL; static uint8 s_Target[2]; int iResult = 1; /* ---- 32 位:Exchange / And / Or / Xor ---- */ xrtAtomic32Init(&A32, 0x0Fu); iOld32 = xrtAtomic32Exchange(&A32, 0x3Cu, XMEMORY_ACQ_REL); if ( (iOld32 != 0x0Fu) || (xrtAtomic32Load(&A32, XMEMORY_ACQUIRE) != 0x3Cu) ) { goto Cleanup; } iOldAnd = xrtAtomic32FetchAnd(&A32, 0x0Fu, XMEMORY_ACQ_REL); if ( iOldAnd != 0x3Cu ) { goto Cleanup; } if ( xrtAtomic32Load(&A32, XMEMORY_ACQUIRE) != 0x0Cu ) { goto Cleanup; /* 0x3C & 0x0F = 0x0C */ } iOldOr = xrtAtomic32FetchOr(&A32, 0x06u, XMEMORY_ACQ_REL); if ( iOldOr != 0x0Cu ) { goto Cleanup; } if ( xrtAtomic32Load(&A32, XMEMORY_ACQUIRE) != 0x0Eu ) { goto Cleanup; /* 0x0C | 0x06 = 0x0E */ } iOld32 = xrtAtomic32FetchXor(&A32, 0x0Bu, XMEMORY_ACQ_REL); if ( (iOld32 != 0x0Eu) || (xrtAtomic32Load(&A32, XMEMORY_ACQUIRE) != 0x05u) ) { goto Cleanup; /* 0x0E ^ 0x0B = 0x05 */ } printf("atomic: 32-bit exchange=0F and/or/xor = %X/%X/%X\n", (unsigned)iOld32 & 0xFu, 0xCu, 0x5u); /* ---- 32 位 CAS:成功路径 + 强语义失败回写 ---- */ iExpected = 0x05u; if ( !xrtAtomic32CompareExchange(&A32, &iExpected, 0x0Au, XMEMORY_ACQ_REL, XMEMORY_ACQUIRE) || (xrtAtomic32Load(&A32, XMEMORY_ACQUIRE) != 0x0Au) ) { goto Cleanup; } iExpected = 0x99u; /* 期望错误:失败并把实际值 0x0A 写回 */ if ( xrtAtomic32CompareExchange(&A32, &iExpected, 0x0Bu, XMEMORY_ACQ_REL, XMEMORY_ACQUIRE) || (iExpected != 0x0Au) ) { goto Cleanup; } printf("atomic: cas ok=1 fail-rewrites=%X\n", (unsigned)iExpected); /* ---- 32 位:Store / FetchAdd / FetchSub(加减按模回绕) ---- */ xrtAtomic32Store(&A32, 0x33u, XMEMORY_RELEASE); iOld32 = xrtAtomic32FetchAdd(&A32, 0x0Cu, XMEMORY_ACQ_REL); if ( (iOld32 != 0x33u) || (xrtAtomic32Load(&A32, XMEMORY_ACQUIRE) != 0x3Fu) ) { goto Cleanup; } iOld32 = xrtAtomic32FetchSub(&A32, 0x0Fu, XMEMORY_ACQ_REL); if ( (iOld32 != 0x3Fu) || (xrtAtomic32Load(&A32, XMEMORY_ACQUIRE) != 0x30u) ) { goto Cleanup; } /* ---- 64 位:Init / Exchange / Sub / And / Or / Xor ---- */ xrtAtomic64Init(&A64, UINT64_C(100)); iOld64 = xrtAtomic64Exchange(&A64, UINT64_C(50), XMEMORY_ACQ_REL); if ( iOld64 != UINT64_C(100) ) { goto Cleanup; } iOld64 = xrtAtomic64FetchSub(&A64, UINT64_C(20), XMEMORY_ACQ_REL); if ( (iOld64 != UINT64_C(50)) || (xrtAtomic64Load(&A64, XMEMORY_ACQUIRE) != UINT64_C(30)) ) { goto Cleanup; } iOld64 = xrtAtomic64FetchAnd(&A64, UINT64_C(0x1C), XMEMORY_ACQ_REL); if ( (iOld64 != UINT64_C(30)) || (xrtAtomic64Load(&A64, XMEMORY_ACQUIRE) != UINT64_C(28)) ) { goto Cleanup; /* 30 & 28 = 28 */ } iOld64 = xrtAtomic64FetchOr(&A64, UINT64_C(3), XMEMORY_ACQ_REL); if ( (iOld64 != UINT64_C(28)) || (xrtAtomic64Load(&A64, XMEMORY_ACQUIRE) != UINT64_C(31)) ) { goto Cleanup; /* 28 | 3 = 31 */ } iOld64 = xrtAtomic64FetchXor(&A64, UINT64_C(31), XMEMORY_ACQ_REL); if ( (iOld64 != UINT64_C(31)) || (xrtAtomic64Load(&A64, XMEMORY_ACQUIRE) != 0u) ) { goto Cleanup; /* 31 ^ 31 = 0 */ } xrtAtomic64Store(&A64, UINT64_C(7), XMEMORY_RELEASE); if ( xrtAtomic64Load(&A64, XMEMORY_ACQUIRE) != UINT64_C(7) ) { goto Cleanup; } printf("atomic: 64-bit init/exchange/sub/and/or/xor = %llu\n", (unsigned long long)xrtAtomic64Load(&A64, XMEMORY_ACQUIRE)); /* ---- 指针:Init / Exchange / CompareExchange ---- */ xrtAtomicPtrInit(&APtr, (ptr)&s_Target[0]); pOld = xrtAtomicPtrExchange(&APtr, (ptr)&s_Target[1], XMEMORY_ACQ_REL); if ( (pOld != (ptr)&s_Target[0]) || (xrtAtomicPtrLoad(&APtr, XMEMORY_ACQUIRE) != (ptr)&s_Target[1]) ) { goto Cleanup; } pExpected = (ptr)&s_Target[0]; /* 错误期望:失败回写 */ if ( xrtAtomicPtrCompareExchange(&APtr, &pExpected, (ptr)&s_Target[0], XMEMORY_ACQ_REL, XMEMORY_ACQUIRE) || (pExpected != (ptr)&s_Target[1]) ) { goto Cleanup; } pExpected = (ptr)&s_Target[1]; if ( !xrtAtomicPtrCompareExchange(&APtr, &pExpected, (ptr)&s_Target[0], XMEMORY_ACQ_REL, XMEMORY_ACQUIRE) ) { goto Cleanup; } /* Store:CAS 后直接写回并核对。 */ xrtAtomicPtrStore(&APtr, (ptr)&s_Target[1], XMEMORY_RELEASE); if ( xrtAtomicPtrLoad(&APtr, XMEMORY_ACQUIRE) != (ptr)&s_Target[1] ) { goto Cleanup; } printf("atomic: ptr exchange/cas = ok\n"); /* ---- 杂项:无锁判定 / 栅栏 / Pause ---- */ if ( !xrtAtomicIsLockFree(sizeof(uint32)) || !xrtAtomicIsLockFree(sizeof(uint64)) ) { goto Cleanup; } xrtAtomicThreadFence(XMEMORY_ACQ_REL); xrtAtomicSignalFence(XMEMORY_SEQ_CST); xrtAtomicPause(); printf("atomic: lockfree(4/8)=1 fence+pause ok\n"); iResult = 0; Cleanup: return iResult; }
刚才发生了什么。 ① 三种宽度各走一遍 Exchange 与位运算族——"Fetch 返回旧值"在每个断言里都是直接证据(FetchAnd 返回 0x3C,新值 0x0C 是它和掩码的与);② 第二段输出专门演示 CAS 失败回写:期望 0x99 不匹配,回写实际值 0x0A;③ 指针宽度用静态缓冲区地址做交换与 CAS——"当前指向谁"的发布场景(无锁栈顶、实例切换)就是它的实战位;④ 收尾的 IsLockFree + ThreadFence + SignalFence + Pause 把辅助件一次点亮。这个程序也是很好的"迁移自检":换平台先跑它,五个输出行全对说明原子层行为一致。
完整程序三:自旋锁的两种生命周期
#include <xrt.h> #include <stdio.h> */ /* 展示栈上短临界区锁的生命周期。 */ int main(void) { xspinlock Spin; uint64 iCounter = 0u; if ( !xrtSpinInit(&Spin) ) { return 1; } if ( !xrtSpinLock(&Spin) ) { return 2; } iCounter++; if ( !xrtSpinUnlock(&Spin) || !xrtSpinUnit(&Spin) ) { return 3; } printf("counter=%llu\n", (unsigned long long)iCounter); /* 堆形态补集:Create / TryLock 持有与空闲正反 / Destroy。 */ { xspinlock* pHeap = xrtSpinCreate(); if ( (pHeap == NULL) || !xrtSpinLock(pHeap) || xrtSpinTryLock(pHeap) || /* 已持有:Try 必失败 */ !xrtSpinUnlock(pHeap) || !xrtSpinTryLock(pHeap) || /* 空闲:Try 必成功 */ !xrtSpinUnlock(pHeap) || !xrtSpinDestroy(pHeap) ) { return 4; } } printf("heap try=0/1 destroy ok\n"); return 0; }
刚才发生了什么。 ① 栈形态走 Init → Lock → 临界区(一条自增)→ Unlock → Unit 全周期——临界区短到只有一条指令,这正是自旋的合法区间;② 堆形态补齐 Create/TryLock/Destroy:持有状态下 TryLock 必失败、空闲后必成功——两态各验一次;③ 例子里锁保护的是演示性的单变量——真实场景若真只是计数器,答案是原子而非锁,示例锁的是"展示生命周期"这件事本身。第 53 章会把自旋放进"自旋 vs 互斥 vs 无锁队列"的三方对照里计时。
完整程序四:deadline 的剩余量与过期判定
/* 相对毫秒等待和 double 秒计时。 */ #include <stdio.h> #include <xrt.h> int main(void) { double Start = xrtTimer(); xrtSleep(50); printf("elapsed_s=%.6f forever_ms=%lld\n", xrtTimer() - Start, (long long)XRT_WAIT_FOREVER); return 0; }
xrtSleep(50) 等待至少 50 毫秒;前后 xrtTimer() 相减得到 double 秒耗时。
9.4契约
- 原子性边界:原子操作保证单变量单步无交错;两次原子操作之间没有整体原子性——跨变量的不变式归锁(第 53 章),所有权传递归队列(第 21 章)。
- Fetch 语义:一律返回操作前的旧值;新值由调用方自行推导,禁止"回头再 Load"的惯性写法。
- CAS 回写:成功不动
*pExpected;失败把实际观察值写入*pExpected——重试循环据此修正,无需补读。 - 内存序合法性:Load ∈ {RELAXED, ACQUIRE, SEQ_CST};Store ∈ {RELAXED, RELEASE, SEQ_CST};CAS 失败序不含 RELEASE 且不得强于成功序。非法组合
XERR_ARGUMENT,对象保持不变。 - 顺序下限承诺:实现只允许强于文档顺序、不允许弱化——依赖文档写作,不为具体平台写作。
- 默认纪律:未证明可放宽前用 SEQ_CST;降级须有性能依据(第 136 章方法)并注释理由。
- 自旋锁生命周期:栈 Init/Unit、静态
XRT_SPIN_INIT、堆 Create/Destroy;释放仍被持有的锁失败并置XERR_STATE;TryLock 非阻塞、已持有返回假。 - 等待参数:
int64毫秒值按值传递,无所有权;0表示只尝试一次,XRT_WAIT_FOREVER表示无限等待,其他负数无效。 - xwaitresult 五态互斥:ERROR/OK/TIMEOUT/CANCELLED/CLOSED;超时与取消是控制流不是错误,不进错误链。
- 线程规则:原子对象无部分更新状态(任何观测要么旧要么新);栅栏与 Pause 对所有平台可移植。
常见错误
症状:消费线程看到"数据就绪"标志后读到的仍是旧数据;问题概率性出现,换 CPU 或编译器选项后频率变化。
原因:数据写入与标志写入都用了 RELAXED(或根本是普通写)——两者之间没有顺序承诺,标志可以先于数据可见。"发布—获取"配对缺了一侧,承诺就不存在。
Data = Compute(); /* 普通写 */ xrtAtomic32Store(&Ready, 1u, XMEMORY_RELAXED); /* 弱序发布:数据未必已可见 */
Data = Compute(); /* 写数据 */ xrtAtomic32Store(&Ready, 1u, XMEMORY_RELEASE); /* 发布:此前写入全部就绪 */ /* 消费侧配对:xrtAtomic32Load(&Ready, XMEMORY_ACQUIRE) 之后读 Data 必为发布版 */
症状:负载一上来 CPU 占满、吞吐反而下降;性能剖析显示热点在 xrtSpinLock 的忙等循环上。
原因:临界区里放了 IO、内存分配或长计算——忙等的线程不干活还抢核,持锁线程被拖慢,等待变长,恶性循环。自旋的前提是"临界区只有几条指令"。
xrtSpinLock(&Lock); SaveToDisk(&Record); /* 临界区含 IO:毫秒级——自旋者烧掉整个核 */ xrtSpinUnlock(&Lock);
xrtSpinLock(&Lock); iCount++; /* 几条指令:自旋的合法区间 */ xrtSpinUnlock(&Lock); /* 长活用互斥(第 53 章 mutex)——睡眠等待不烧 CPU;或先算好再进短临界区提交 */
症状:写着"最多重试十秒"的循环实际跑了二十几秒;每轮等待预算都被重置,总时长随轮数发散。
原因:把"每轮等多久"当成了"总共等多久"。相对超时只约束单轮,循环把它放大成 N 倍;系统真正能承诺的是绝对截止时刻。
for ( i = 0; i < 100; i++ ) { if ( TryOnce() ) { break; } Sleep(100000); /* 每轮重置预算:100 轮可达 10 秒以上 */ }
double D = xrtTimer() + 10.0; /* 循环外一次构造 */ while ( !TryOnce() ) { if ( (xrtTimer() >= D) ) { return XWAIT_TIMEOUT; } WaitOnce(((int64)ceil(fmax(0.0, D - xrtTimer()) * 1000.0))); /* 剩余量递减,绝不重置 */ } return XWAIT_OK;
编程练习
用 xrtAtomic64FetchAdd(RELAXED)让四个线程各累加一百万次,主线程汇合后 Load 校验总数恰为四百万。验收标准:总数恒定(每次运行一致);把原子换成普通 ++ 重跑,观察丢更新。
结构体含 Done 与 Value 两个字段;用 xrtAtomic32CompareExchange 实现"仅当 Done 为 0 时置 1 并写 Value"的无锁迁移——失败时利用期望值回写修正重试。验收标准:多线程竞争下恰好一个线程成功;全程无锁。
同一计数任务三版实现:原子版、自旋锁版、mutex 版(第 53 章 API),各计时并对比;再分别把临界区拉长(加一段计算)复测,观察自旋版优势何时反转。验收标准:得出"几条指令内自旋占优、变长后互斥占优"的量化拐点;结论与第 53 章分工一节互证。
小结
| 知识点 | 速查 |
|---|---|
| 定位 | 卷一收官:原子原语 + 自旋锁 + 等待数学——全书并发的公共底座 |
| 三宽度 | xatomic32/64/ptr;Init 宏(静态)与函数(运行期) |
| Fetch 语义 | 返回旧值;新值自行推导,不回头 Load |
| CAS 回写 | 成功不动期望;失败回写实际值——重试的起点 |
| 内存序 | RELAXED/ACQUIRE/RELEASE/ACQ_REL/SEQ_CST;默认 SEQ_CST,降级要有据 |
| 发布—获取 | 写侧 RELEASE、读侧 ACQUIRE 配对;任一 RELAXED 即失效 |
| 辅助件 | ThreadFence/SignalFence/Pause/IsLockFree |
| 自旋锁 | 几条指令临界区专用;释放持锁 → XERR_STATE;TryLock 非阻塞 |
| deadline | 绝对截止(单调秒);循环外 After 一次、循环内 Remaining |
| xwaitresult | ERROR/OK/TIMEOUT/CANCELLED/CLOSED 五态;超时取消是控制流 |
| 分工线 | 单变量→原子;不变式→锁;传指针→队列(第 21 章) |
📖 本章涉及的 API 参考手册:atomic(30 个条目)spin(8 个条目)wait(? 个条目)
点击查看完整签名、参数约束、返回值与错误说明