问题描述
我正在 Kotlin 中学习协程,我有一段看起来像这样的代码(见下文)。
我朋友说 mutableMapOf 是 LinkedHashMap,它不是线程安全的。论据是挂起函数可能由不同的线程运行,因此LinkedHashMap不合适。
- 在这里使用简单的可变映射安全还是需要 ConcurrentMap?
- 当一个挂起函数被挂起时,它可以被另一个线程恢复和执行吗?
- 即使 (2) 是可能的,是否有“happens-before/happens-after”保证以确保所有变量(和底层对象内容)在新线程接管之前从主内存深度同步?立>
这是代码的简化版本:
class CoroutineTest {
private val scope = Coroutinescope(SupervisorJob() + dispatchers.Default)
suspend fun simpleFunction(): MutableMap<Int,String> {
val myCallResults = mutableMapOf<Int,String>()
val deferredCallResult1 = scope.async {
//make rest call get string back
}
val deferredCallResult2 = scope.async {
//make rest call get string back
}
...
myCallResults.put( 1,deferredCallResult1.await() )
myCallResults.put( 2,deferredCallResult2.await() )
...
return myCallResults
}
}
提前致谢!
附注。我用更多的异步调用结果运行了这段代码,没有问题;所有调用结果都被考虑在内。但这可能是不确定的,这就是我问的原因。
解决方法
- 不,在多个协程中使用单个
mutableMapOf()
是不安全的。 - 您对暂停的理解不正确。这不是暂停的功能。在函数中运行的协程可能会挂起。从这个角度来看,挂起函数与普通函数并没有真正的不同 - 它们可以由许多协程同时执行,并且所有协程都将同时工作。
但是……由于另一个原因,您的代码没有任何问题。这个可变映射是一个局部变量,所以它只对创建它的协程/线程可用。因此,它根本不是并发访问的。如果地图是 CoroutineTest
的属性,情况会有所不同 - 那么这可能意味着您需要使用 ConcurrentMap
。
更新
阅读所有评论后,我相信我对您(或您的朋友)的担忧有了更好的了解,因此我可以提供更准确的答案。
是的,在挂起一个协程之后,它可以从另一个线程恢复,所以协程可以让一个函数的一部分由一个线程执行,另一部分由另一个线程执行。在您的示例中,可能会从两个不同的线程调用 put(1
和 put(2
。
然而,说 LinkedHashMap
不是线程安全的并不意味着它必须始终由同一个线程访问。它可以被多个线程访问,但不能同时。一个线程需要完成对地图的更改,然后另一个线程才能执行其修改。
现在,在您的代码中,async { }
块可以并行工作。它们还可以与外部作用域并行工作。但是它们每个的内容都是按顺序工作的。 put(2
行只能在 put(1
完全完成后执行,因此地图不会被多个线程同时访问。
如前所述,如果地图将被存储,例如作为一个属性,simpleFunction()
会修改它,并且这个函数会被并行调用多次——然后每次调用都会尝试同时修改它。如果异步操作直接修改 myCallResults
也会有所不同。正如我所说,异步块彼此并行运行,因此它们可以同时修改映射。但由于您只从异步块返回结果,然后从单个协程(从外部作用域)修改映射,因此映射是按顺序访问的,而不是并发访问。
由于映射是挂起函数的本地映射,因此使用非线程安全的实现是安全的。不同的线程可能会使用不同挂起函数调用(在本例中为 await()
调用)之间的映射,但保证在挂起函数内发生之前/之后发生。
如果您的地图是在挂起函数之外声明并通过属性访问的,那么可能会同时调用此函数,并且您将同时修改它,这将是一个问题。