如何规避MVar使用过程中出现Race condition竞态条件的不安全用法
问题本质
你遇到的风险核心是MVar仅保证访问权的互斥持有,不会限制内部存储值的可变性。只要可变对象的引用被泄露到MVar的take/put生命周期外,就可以绕开MVar的互斥保护直接修改内部状态,和示例里子fiber泄露引用的场景完全一致。
可行的规避方案
方案1:优先使用不可变数据结构(最推荐)
不要往MVar里存入任何可变对象,全部替换为Scala标准库的不可变集合。不可变对象本身无法被修改,任何修改操作都会生成新的实例,就算引用泄露也不会影响MVar中存储的原值。
修改后的示例代码如下:implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global) implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global) // 存入不可变Map val mvarF = MVar.of[IO, Map[Int, Int]](Map.empty) mvarF.flatMap(mvar => mvar.take.bracket(st => { // 修改生成新的Map实例 val newSt = st + (1 -> 1) // 子fiber就算持有旧的st引用也无法修改,只能修改自己的副本 IO(newSt) >> (IO.sleep(2.seconds) >> IO(println("子 fiber 无法修改MVar内的旧值"))).start })(newSt => mvar.put(newSt)) // 把新实例放回MVar >> mvar.take.bracket(st => IO(println(s"Size before sleep ${st.size}")) >> IO.sleep(2.seconds) >> IO(println(s"Size after sleep ${st.size}")) )(mvar.put) ).unsafeRunSync()运行后两次打印的size都是1,不会出现被意外修改的问题。
方案2:对可变对象做保护性拷贝
如果业务场景必须使用可变对象,每次从MVar取到对象后生成一个独立的可变副本,操作完成后把副本放回MVar,就算原始引用被泄露也无法影响MVar内部存储的对象:// take后先拷贝 mvar.take.flatMap(originalSt => { val copySt = mutable.Map.from(originalSt) // 后续所有操作都用copySt,最后put的时候也放copySt })方案3:改用
Ref做原子状态管理
对于简单的状态读写场景,可以用Cats Effect提供的Ref类型替代MVar,Ref的修改操作是原子的,天然避免了手动take/put配对带来的引用泄露风险,同样建议搭配不可变数据结构使用:val refF = Ref.of[IO, Map[Int, Int]](Map.empty) refF.flatMap(ref => // 原子修改,不会出现引用泄露 ref.update(_ + (1 -> 1)) >> ref.get.flatMap(st => IO(println(s"当前size: ${st.size}"))) ).unsafeRunSync()
内容的提问来源于stack exchange,提问作者Some Name
相关产品推荐
相关产品推荐

