Rust 所有权与生命周期从入门到实战:并发上来后先守住哪条线

Rust 所有权与生命周期从入门到实战:并发上来后先守住哪条线

并发任务共享状态时,先划清谁能修改。计数器适合 AtomicUsize,复杂 map 则用 Mutex,不要为了省事把所有东西都塞进同一把锁。

let count = Arc::new(AtomicUsize::new(0));
count.fetch_add(1, Ordering::Relaxed);

我会在测试中开多个任务并断言最终数量。这个例子只说明计数正确,不保证业务顺序;若顺序重要,要用 channel 或显式状态机。共享数据不要包含原始用户输入。

先把共享范围缩到最小

刚接触并发时,最容易出现的写法是把整个业务对象包进 Arc<Mutex<_>>,每个任务拿到同一份句柄,再在需要时上锁。这段代码通常能编译,也容易让人误以为问题已经解决。真正麻烦的是锁的边界会慢慢扩大:读取配置时要锁,更新缓存时要锁,组装返回值时也带着锁。某个分支一旦在持锁期间等待 I/O,其他任务就会排队,吞吐下降只是表面现象,更难查的是等待关系已经散落在不同函数里。

我更愿意先把状态拆成两类。能由输入重新计算的值,不要存成共享可变状态;每个任务自己的临时结果,留在任务栈里即可。只有确实需要汇总或协调的部分才共享。例如累计成功次数可以用原子计数,任务间传递一条处理结果可以发到 channel,配置初始化后不再变更则让它以只读方式共享。这样做不是追求“完全不用锁”,而是让锁只保护一个明确的数据结构。

在 Rust 里,所有权检查能阻止一部分误共享,但它不会替你决定业务上的临界区。Arc 说明同一份数据可以被多个所有者引用;Mutex 说明同一时刻只能有一个人修改里面的值。两者组合后,仍要回答一个问题:一次修改需要和哪些字段保持一致?如果订单状态、重试次数和错误原因必须一起更新,把它们分成三把锁反而会让中间状态暴露出去。反过来,互不关联的统计项也没有必要排队等待同一把大锁。

原子操作不等于完整的并发协议

fetch_add 很适合做计数,但它只保证这一次加法是原子的。假设任务需要先检查配额,再扣减配额,最后创建工作项,三个动作分开写成原子操作仍可能被其他任务穿插。此时问题不是 AtomicUsize 用错了,而是业务操作本身需要一个更完整的协议。可以把决定权集中到单个任务中,由它接收请求并维护状态;也可以用锁把检查与更新包成同一个临界区。选择哪种方式,要看操作是否需要排队、是否要返回即时结果,以及失败后如何恢复。

内存序也不该凭习惯升级到最强。纯粹的统计指标,如果不依赖其他内存写入,Relaxed 往往足够;一旦计数值被拿来发布对象是否初始化完成,就必须重新审视读写之间的可见性关系。这里没有一个可复制到所有项目的固定答案。把每个原子变量旁边的用途写清楚,比堆叠更强的排序更可靠。

测试要覆盖交错,而不只覆盖结果

并发 bug 往往不会在一次普通测试里出现。除了检查最终计数,我会让多个任务在同一时间起跑,并在关键路径加入可控的让出点,让不同执行顺序有机会发生。对带锁的代码,测试重点是锁能否正常释放、错误返回后状态是否仍然可用;对 channel,重点是发送方提前结束、接收方停止消费时是否能退出。测试结束还要等待任务全部收尾,否则后台任务残留会掩盖资源泄漏。

日志里不要直接记录共享对象的完整内容,尤其是对象可能带有用户输入或凭证片段时。调试时记录任务标识、状态转换和计数变化通常已经够用。出现卡顿后,先确认持锁范围内是否有 await、网络调用或磁盘操作,再看是否存在锁顺序不一致。把这几件事在代码评审阶段问一遍,比线上靠堆栈猜死锁原因轻松得多。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值