Kotlin协程下SdkWrapper初始化并发安全问题及解决问询
Let's break down your problem clearly: you need to guarantee three core requirements:
Sdk.init()is called exactly onceuseSdk()only runs afterSdk.init()completes successfully- Concurrent calls to
doSomething()don't break either rule, even when triggered rapidly in succession
Your initial attempts ran into issues because synchronized isn't compatible with coroutine suspend points, and naive flag checks don't account for in-progress initialization. Here are two robust, coroutine-friendly solutions:
Solution 1: Use Mutex (Simple & Straightforward)
Kotlin Coroutines provides Mutex—a suspending lock designed to work with suspend functions without blocking threads. This is the simplest fix for your scenario:
class SdkWrapper(private val sdk: Sdk) { private var inited = false // Create a mutex to guard the initialization logic private val initMutex = Mutex() suspend fun doSomething() = withContext(Dispatchers.IO) { // Only one coroutine can execute this block at a time initMutex.withLock { if (!inited) { sdk.init() inited = true } } // By this point, initialization is guaranteed to be complete useSdk() } private fun useSdk() { } } class Sdk { fun init() {} }
How this works:
initMutex.withLockensures exclusive access to the initialization logic. The first coroutine to calldoSomething()acquires the lock, runssdk.init(), and marksinitedas true.- Subsequent coroutines suspend (instead of blocking threads) until the lock is released, then skip initialization entirely once they see
inited = true. - The outer
withContext(Dispatchers.IO)ensures all operations run on an IO thread, andMutexplays nicely with coroutine context switches.
Solution 2: Use CompletableDeferred (For Advanced Error Handling)
If you need to handle cases where sdk.init() might fail (and want to retry or propagate errors), using a CompletableDeferred with an atomic reference is a more flexible option. This tracks initialization state atomically:
import kotlinx.coroutines.CompletableDeferred import java.util.concurrent.atomic.AtomicReference class SdkWrapper(private val sdk: Sdk) { // Track the initialization job atomically to prevent duplicates private val initJob = AtomicReference<CompletableDeferred<Unit>?>(null) suspend fun doSomething() = withContext(Dispatchers.IO) { val deferred = initJob.get() ?: run { val newDeferred = CompletableDeferred<Unit>() // Atomically set the deferred only if it's null (prevents duplicate init) if (initJob.compareAndSet(null, newDeferred)) { try { sdk.init() newDeferred.complete(Unit) } catch (e: Exception) { newDeferred.completeExceptionally(e) // Optional: Reset the job to allow retries if initialization fails initJob.set(null) } } else { // Another coroutine already started initialization initJob.get()!! } } // Wait for initialization to finish (success or failure) deferred.await() useSdk() } private fun useSdk() { } } class Sdk { fun init() {} }
How this works:
AtomicReferenceensures only the first coroutine can create aCompletableDeferredand start initialization.- All subsequent coroutines wait for the existing
deferredto complete before runninguseSdk(). - If initialization fails, you can choose to reset the
initJobto allow future calls to retry, or leave it to propagate the exception to all waiting coroutines.
Why Your Previous Attempts Failed
Let's quickly recap why your initial approaches didn't work:
- Naive flag check: When two coroutines call
doSomething()rapidly, both seeinited = falsebefore the first finishessdk.init(), leading to duplicate initialization. - Setting
initedbeforesdk.init(): The second coroutine seesinited = trueimmediately and runsuseSdk()beforesdk.init()completes, violating the precondition. - Using
synchronized:synchronizedis a thread-level lock, butwithContextswitches threads. The lock gets released when the coroutine suspends, so subsequent coroutines can enter the block before initialization finishes. IntelliJ's error correctly flags this as unsafe.
内容的提问来源于stack exchange,提问作者fweigl

