kotlin 对于同时具有挂起和非挂起功能的API,建立API线程安全的正确方法是什么?

y1aodyip  于 2023-03-03  发布在  Kotlin
关注(0)|答案(1)|浏览(141)

假设我有以下线程安全类(注意没有suspend函数):

@AnyThread
class MyClass {

    @AnyThread
    fun foo(): Int {
        runWithoutConcurrency()
        // .. the rest
    }

    @AnyThread
    fun bar(): Int {
        runWithoutConcurrency()
        // .. the rest
    }

    @Synchronized
    private fun runWithoutConcurrency() { /* .. */ }
}

假设有一个临界区(它本身不是线程安全的),所以它被放在runWithoutConcurrency()并标记为@Synchronized,这样整个类都是线程安全的。
现在让我们将foo()改为suspend函数,如何实现相同的线程安全性?
添加了更多详细信息:
如果API仅由suspend函数组成,那么我将使用Kotlin协程Mutex,因此这里的问题是如何将Java同步(无论如何都需要使bar()线程安全)与Mutex用法(无论如何都需要使suspend foo()线程安全)“结合”起来。

rqmkfv5c

rqmkfv5c1#

Java中的同步块是通过阻塞线程来等待的,所以我们通常应该尽量避免从协程调用它们。如果没有其他选择,那么根据具体情况,我们可以使用MutexDispatchers.IO或它们的混合。
1..如果synchronized方法只在一个地方被调用,或者我们控制所有这样的地方,并且所有这些地方都挂起-我们可以使用Mutex

mutex.withLock {
    runWithoutConcurrency()
}

我相信这应该是安全的,因为如果我们保证不并发调用runWithoutConcurrency(),那么它就不会阻塞,当然,我们必须在所有调用点使用完全相同的mutex对象。
2..正如@Tenfour04所建议的,如果我们同时从协程和非协程上下文中调用方法,但我们仍然控制所有调用点(如您的示例所示),我们可以选择仍然使用基于互斥锁的解决方案,对于非挂起上下文,使用runBlocking()启动协程。

private val mutex = Mutex()

suspend fun foo(): Int {
    mutex.withLock {
        runWithoutConcurrency()
    }
}

fun bar(): Int {
    runBlocking {
        mutex.withLock {
            runWithoutConcurrency()
        }
    }
}

这里的想法是使用相同的同步机制同步这两种情况,并根据情况选择挂起或阻塞。来自foo()的线程永远不会阻塞,只会挂起。如果runWithoutConcurrency()正在使用,来自bar()的线程可能会阻塞,但这与使用synchronized方法本身是相同的。
3..如果我们无法控制synchronized方法的所有调用点,或者无法轻松地将互斥锁传递到所有调用点,我们可以像处理其他阻塞代码一样处理这种情况--使用Dispatchers.IO

withContext(Dispatchers.IO) {
    runWithoutConcurrency()
}

在这种情况下,IO可能听起来违反直觉,因为我们在这里不做任何I/O,但事实上,Dispatchers.IO可以/应该用于任何类型的阻塞代码,而不仅仅是I/O。我仍然建议如果可能的话将其放在互斥锁中-这样我们最多阻塞一个IO线程,而不是每个调用一个线程。
如果有多个@Synchronized方法,情况会稍有不同,因为它们都使用同一个对象进行同步,但一般规则仍然适用。

相关问题