协程共享状态:Atomic、Mutex、Semaphore与状态封闭

协程共享状态:Atomic、Mutex、Semaphore与状态封闭

协程让异步代码更容易组织,却不会自动消除并发安全问题。只要多个协程可能同时读写同一份可变数据,原子性、可见性和一致性就仍然需要明确保障。

并发工具的选择不能从API名称出发,而要先判断需要约束的对象:一个变量、一段复合逻辑、同时运行的任务数量,还是状态本身的所有权。Atomic、Mutex、Semaphore和状态封闭分别对应这四种不同问题。

内容摘要

  1. count++包含读取、计算和写回,并不是原子操作。
  2. Atomic适合单个变量的简单原子更新,不负责维护复杂业务不变量。
  3. Mutex保护临界区,等待锁时挂起协程而不是阻塞线程。
  4. Semaphore限制同时进入某段逻辑的协程数量,解决的是并发度控制。
  5. 状态封闭通过明确唯一所有者,从设计上减少共享写入。

一、并发问题来自共享可变状态

一行代码也可能包含多个步骤

javascript 复制代码
var count = 0
​
scope.launch(Dispatchers.Default) {
    repeat(1_000) {
        // count++ 包含读、改、写,多协程并发执行时可能丢失更新
        count++
    }
}

count++可以拆成三个动作:

复制代码
读取旧值
→ 计算新值
→ 写回新值

两个协程可能同时读取相同旧值,各自加一后又写回相同新值,最终丢失一次更新。代码是否写在协程中并不改变这条事实;协程最终仍由线程执行,多个任务也可能在不同线程上并行运行。

并发安全不只涉及原子性。缺少同步关系时,还可能出现一个执行单元无法及时看到另一个执行单元更新,以及多个操作的观察顺序不符合预期等问题。具体工具需要同时承担它所承诺的同步语义,不能仅凭"看起来按顺序书写"判断安全。

先识别需要保持的不变量

真正需要保护的通常不是某个语句,而是业务不变量:

复制代码
库存不能小于零
缓存值与更新时间必须对应
同一资源同时最多执行三个请求
状态只能按合法路径迁移

只有一个计数值需要安全加一,与"读取缓存、判断是否过期、写入新缓存"不是同一类问题。工具选错后,局部操作即使线程安全,整体规则仍可能被破坏。

本节小结

协程并发安全的起点不是加锁,而是找出共享可变状态和必须始终成立的业务不变量。单行代码不等于原子操作,局部安全也不等于整体一致。

二、Atomic与Mutex保护不同粒度

Atomic:单个变量的原子更新

scss 复制代码
// AtomicInteger 为单个整数提供原子更新能力
val count = AtomicInteger(0)
​
// 加一与读取结果作为一次不可分割的原子操作完成
count.incrementAndGet()

Atomic通常基于CAS完成更新:

复制代码
读取当前值作为预期旧值
→ 计算新值
→ 仅当当前值仍等于预期旧值时写入
→ 失败后重新读取并重试

它适合计数器、开关、单一引用替换等简单状态。优势是更新范围小,不需要让其他协程排队进入一段代码。

但多个原子变量无法天然组成一次原子事务:

scss 复制代码
// 两个变量分别原子,不代表它们能够作为整体同时更新
val balance = AtomicInteger(100)
val version = AtomicInteger(1)

分别更新balance和version都是原子的,不代表二者在任何观察时刻都保持一致。只要业务规则跨越多个字段或多个步骤,就需要更高层的状态对象或临界区。

Mutex:让复合逻辑互斥执行

kotlin 复制代码
// Mutex 保护"读取、判断、更新"这一整段复合逻辑
private val mutex = Mutex()
​
suspend fun loadConfig(): Config = mutex.withLock {
    // 同一时刻只有一个协程能够进入该临界区
    cache.current()
        ?.takeIf { !it.isExpired }
        // 缓存失效后加载远端数据,并在同一临界区内回写缓存
        ?: remote.load().also(cache::save)
}

Mutex保证同一时间最多一个协程进入临界区。等待锁的协程会挂起,不会占住线程持续等待,这是它与阻塞式锁的重要运行差异。

临界区应尽量保持紧凑,并通过withLock保证异常或取消发生时正确释放。是否允许在锁内执行耗时挂起操作,需要结合业务判断:技术上可以挂起,但长时间持锁会让其他协程持续等待,也可能把远程调用的不确定延迟引入共享状态路径。

不要用锁修补不清晰的状态边界

如果多处代码都能直接修改同一对象,即使每处都加锁,状态迁移规则仍然分散。锁解决同时执行的问题,不会自动建立合法状态机,也不会说明谁有权修改状态。

本节小结

Atomic保护单个变量的一次更新,Mutex保护一段复合逻辑。判断标准是业务不变量跨越的范围,而不是哪种工具代码更短。

三、Semaphore控制的是资源并发度

kotlin 复制代码
// 三个许可表示最多允许三个上传任务同时执行
private val uploadSlots = Semaphore(permits = 3)
​
suspend fun upload(file: File): UploadResult =
    uploadSlots.withPermit {
        // 代码块结束后自动归还许可,异常和取消也不会永久占用名额
        api.upload(file)
    }

Semaphore维护一组许可。只有拿到许可的协程才能进入目标逻辑,结束后归还许可。因此它适合:

  • 限制并发网络请求,避免服务端或连接池压力过高;
  • 限制图片解码、文件处理等资源密集任务;
  • 控制批量任务的吞吐和内存峰值。

当许可数为1时,Semaphore在表面上也能实现互斥,但语义仍然不同:

复制代码
Mutex     → 保护共享状态的临界区
Semaphore → 为有限资源控制并发名额

如果目标是保护某个变量,应表达为互斥;如果目标是允许最多N个任务并发,应表达为许可。清晰的语义会直接影响后续维护和参数调优。

还需要注意,限制并发数量不等于限制单位时间内的请求总数。Semaphore(3)保证最多三个任务同时运行,却不会保证每秒只发三个请求;后者属于速率限制,需要单独的时间窗口或令牌策略。

本节小结

Semaphore保护的不是变量,而是有限资源的使用容量。它控制同时运行多少任务,不自动保证共享状态安全,也不等同于单位时间限流。

四、状态封闭把问题收口到所有权

让状态只有一个写入入口

状态封闭的核心不是"换一种锁",而是让状态只归一个所有者管理:

kotlin 复制代码
class DownloadStore {
    // 状态只在 Store 内部修改,并由 Mutex 串行化状态迁移
    private val mutex = Mutex()
    private var state: DownloadState = DownloadState.Idle

    suspend fun reduce(action: DownloadAction): DownloadState =
        mutex.withLock {
            // 根据旧状态和动作生成新状态,外部不能直接改写 state
            state = transition(state, action)

            // 返回更新后的不可见内部状态快照
            state
        }
}

外部只能提交动作或调用方法,不能直接改写state。这样一来,并发控制、合法状态迁移和日志记录都集中在同一边界内。

更严格的设计可以让一个专属协程顺序处理消息:

复制代码
多个调用方发送命令
→ 单一所有者按顺序处理
→ 更新内部状态
→ 对外发布不可变快照

这种模型把"谁先拿到锁"转化为"命令按什么规则处理"。如果状态本身具有复杂生命周期,它通常比在各处零散加锁更容易验证。

一套稳定的选择顺序

sql 复制代码
能否取消共享写入?
├─ 能:优先状态封闭或不可变数据
└─ 不能
   ├─ 单个变量简单更新:Atomic
   ├─ 多步逻辑保持一致:Mutex
   └─ 限制同时运行数量:Semaphore

这些方案并不互斥。一个状态所有者内部仍可能用Mutex保证串行更新,批量请求外部再用Semaphore控制并发。关键是每种工具只承担清晰的一层责任。

本节小结

状态封闭通过唯一所有者减少共享面,并把状态迁移规则集中起来。它解决的是设计边界;Atomic、Mutex和Semaphore解决的是边界内部不同类型的并发约束。

结语:先保护业务规则,再选择同步工具

协程共享状态的稳定判断模型可以归纳为:

sql 复制代码
简单变量原子更新   → Atomic
复合逻辑保持一致   → Mutex
有限资源并发控制   → Semaphore
复杂状态统一管理   → 状态封闭

并发工具不是强弱递进关系,也不能相互机械替代。可靠设计需要先明确共享状态、业务不变量和所有权,再用最贴合问题的机制建立边界。

参考资料

相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui