Middle+
How can synchronization be implemented in Kotlin using coroutines?
sobes.tech AI
Answer from AI
Synchronization in Kotlin coroutines is achieved using special primitives and data structures from the kotlinx.coroutines library.
Main approaches:
-
Mutex
- Provides mutual exclusion to protect critical sections of code.
- A coroutine attempting to acquire a Mutex is blocked until it is released.
- Has functions
lock()andunlock(), as well as a more idiomaticwithLock.
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.* val mutex = Mutex() var counter = 0 suspend fun increment() { mutex.withLock { counter++ } } suspend fun main() = coroutineScope { repeat(10_000) { launch { increment() } } println(counter) // Expect 10000 } -
Semaphore
- Limits the number of coroutines that can access a resource or execute a block of code simultaneously.
- Manages a pool of permits.
- Has functions
acquire()andrelease(), as well aswithPermit.
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.* val semaphore = Semaphore(2) // Up to 2 coroutines can work simultaneously suspend fun doLimitedWork(id: Int) { semaphore.withPermit { println("Coroutine $id acquired a permit. Working...") delay(100) // Simulate work println("Coroutine $id released a permit.") } } suspend fun main() = coroutineScope { repeat(5) { i -> launch { doLimitedWork(i) } } } -
Atomic operations (from
kotlinx.coroutines.atomic)- Provide thread-safe operations on primitive types and references.
- Use low-level CPU instructions (CAS - Compare-and-Swap).
- Suitable for simple operations without explicit locks.
import kotlinx.coroutines.atomic.AtomicInt import kotlinx.coroutines.* val atomicCounter = AtomicInt(0) suspend fun atomicIncrement() { atomicCounter.incrementAndGet() } suspend fun main() = coroutineScope { repeat(10_000) { launch { atomicIncrement() } } println(atomicCounter.value) // Expect 10000 } -
Shared data structures on a single-threaded dispatcher
- The simplest approach. Running coroutines on a single-threaded context (
Dispatchers.Default.limitedParallelism(1)ornewSingleThreadContext) guarantees sequential execution of code, avoiding data races. It is not an explicit synchronization primitive but ensures synchronization by serializing access.
import kotlinx.coroutines.* val mySingleThreadContext = newSingleThreadContext("SingleThread") var sharedData = mutableListOf<Int>() suspend fun addToSharedData(value: Int) { sharedData.add(value) // Safe as it runs on a single thread } suspend fun main() = withContext(mySingleThreadContext) { repeat(10_000) { launch { addToSharedData(it) } } println(sharedData.size) // Expect 10000 } - The simplest approach. Running coroutines on a single-threaded context (
-
Channels
- Not primitives of synchronization in themselves, but can be used for safe data transfer between coroutines, indirectly solving access synchronization issues.
- Provide a way to transfer a stream of data from one coroutine to another.
The choice of approach depends on the use case scenario. Mutex and Semaphore provide classic locking mechanisms, Atomic operations are efficient for simple atomic updates, and a single-threaded dispatcher is convenient when access to shared data must be strictly sequential. Channels are used for communication and coordination, not direct protection of shared resources.