协程非阻塞锁mutex

313 阅读3分钟

避免在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)
        }
    }
}

从日志可以看出,两个任务的enterexit并行输出,并没有先后顺序

#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与否,仍然是并行执行的(enterexit 同时输出 )。

我们换一种写法,在挂起函数内部添加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来保证互斥,可以看做是SynchorinzedLock的替代品,还有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调用后,因为delaysuspend函数,doSomething函数就return退出了,Synchronized也就无效了,所以只有 thread nameenter 在日志上保持了串行,enterexit 仍然是并行输出。