避免在suspend函数中使用
@Synchronized修饰符,而是使用Mutex
一、协程+Synchronized ?
通常,协程可以帮助我们执行并行任务:
suspend fun doSomething(i: Int) {
println("#$i enter critical section")
// do something critical
delay(1000)
println("#$i exit critical section")
}
fun main() = runBlocking {
repeat(2) { i ->
launch(Dispatchers.Default) {
println("#$i thread name: ${Thread.currentThread().name}")
doSomething(i)
}
}
}
从日志可以看出,两个任务的enter和exit并行输出,并没有先后顺序
#0 thread name: DefaultDispatcher-worker-1
#1 thread name: DefaultDispatcher-worker-2
#0 enter critical section
#1 enter critical section
#1 exit critical section
#0 exit critical section
接下来添加@Synchronized试试看:
@Synchronized
suspend fun doSomething(i: Int) {
println("#$i enter critical section")
// do something
delay(1000)
println("#$i exit critical section")
}
fun main() = runBlocking {
repeat(2) { i ->
launch(Dispatchers.Default) {
println("#$i thread name: ${Thread.currentThread().name}")
doSomething(i)
}
}
}
#0 thread name: DefaultDispatcher-worker-2
#0 enter critical section
#1 thread name: DefaultDispatcher-worker-1
#1 enter critical section
#0 exit critical section
#1 exit critical section
对于普通函数,由于Synchronized的添加,两个线程应该顺序执行,但是上面日志显示,对于挂起函数,无论添加Synchronized与否,仍然是并行执行的(enter,exit 同时输出 )。
我们换一种写法,在挂起函数内部添加Synchronized试试:
val LOCK = Object()
suspend fun doSomething(i: Int) {
synchronized(LOCK) {
println("#$i enter critical section")
// do something
delay(1000) // <- The 'delay' suspension point is inside a critical section
println("#$i exit critical section")
}
}
fun main() = runBlocking {
repeat(2) { i ->
launch(Dispatchers.Default) {
println("#$i thread name: ${Thread.currentThread().name}")
doSomething(i)
}
}
}
出现如下编译错误:
"The 'delay' suspension point is inside a critical section"
二、协程同步需使用Mutex
上面实验证明Synchronized无法用在协程同步的场景,协程同步应该使用Mutex
协程中提供了Mutex来保证互斥,可以看做是Synchorinzed和Lock的替代品,还有withLock 扩展函数,可以⽅便替代常⽤的:
mutex.lock()
try {
//do something
}finally {
mutex.unlock()
}
替换为:
mutex.withLock {
//do something
}
具体源码:
@OptIn(ExperimentalContracts::class)
public suspend inline fun <T> Mutex.withLock(owner: Any? = null, action: () -> T): T {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
lock(owner)
try {
return action()
} finally {
unlock(owner)
}
}
所以上面的例子可以改为:
val mutex = Mutex()
suspend fun doSomething(i: Int) {
mutex.withLock {
println("#$i enter critical section")
// do something
delay(1000)
println("#$i exit critical section")
}
}
fun main() = runBlocking {
repeat(2) { i ->
launch(Dispatchers.Default) {
println("#$i thread name: ${Thread.currentThread().name}")
doSomething(i)
}
}
}
#0 thread name: DefaultDispatcher-worker-1
#1 thread name: DefaultDispatcher-worker-2
#1 enter critical section
#1 exit critical section
#0 enter critical section
#0 exit critical section
我们再看一下具体的使用:
suspend fun testMutex() {
var count = 0
val job1 = CoroutineScope(Dispatchers.IO).launch{
repeat(100){
count ++
//delay 1ms是为了避免执行太快
delay(1)
}
println("count1:${count}")
}
val job2 = CoroutineScope(Dispatchers.IO).launch{
repeat(100){
count ++
delay(1)
}
println("count2:${count}")
}
job1.join()
job2.join()
}
我们多次运行看下结果,发现每次输出都不一样:
count2:196
count1:196
我们加上Mutex试一下:注意,对于多个协程来说用的是同一个Mutex
suspend fun testMutex() {
var count = 0
//注意:对于多个协程来说用的是同一个Mutex
val mutex = Mutex()
val job1 = CoroutineScope(Dispatchers.IO).launch{
mutex.withLock(count) {
repeat(100) {
count++
delay(1)
}
}
println("count1:${count}")
}
val job2 = CoroutineScope(Dispatchers.IO).launch{
mutex.withLock(count) {
repeat(100) {
count++
delay(1)
}
}
println("count2:${count}")
}
job1.join()
job2.join()
}
输出结果:几乎同时开启两个协程,去竞争count的锁,job1和job2谁先拿到count的锁几率是相同的
count2:100
count1:200
如果我们只在一个协程中执行mutex,不会影响到另一个协程对count的读取。
三、为什么Synchrnoized无效
前面讲过suspend挂起函数的本质,再看一下开头的例子:
@Synchronized
suspend fun doSomething(i: Int) {
println("#$i enter critical section.")
// do something
delay(1000)
println("#$i exit critical section.")
}
反编译后是这样的:
@Synchronized
fun doSomething(i: Int, cont: Continuation) {
val sm = cont as? ThisSM ?: ThisSM {
val result
}
switch (sm.label) {
case 0:
println("#$i enter critical section.")
sm.label = 1
delay(1000, sm)
case 1:
println("#$i exit critical section.")
}
}
delay调用后,因为delay 是suspend函数,doSomething函数就return退出了,Synchronized也就无效了,所以只有 thread name和 enter 在日志上保持了串行,enter和exit 仍然是并行输出。