Sobes.tech
Middle+

Hoe kan synchronisatie in Kotlin worden geïmplementeerd met behulp van coroutines?

sobes.tech AI

Antwoord van AI

Synchronisatie in Kotlin-coroutines wordt bereikt met behulp van speciale primitieve en datastructuren uit de bibliotheek kotlinx.coroutines.

Belangrijkste benaderingen:

  1. Mutex

    • Biedt onderlinge uitsluiting om kritieke code-secties te beschermen.
    • De coroutine die probeert een Mutex te verkrijgen, wordt geblokkeerd totdat deze wordt vrijgegeven.
    • Beschikt over functies lock() en unlock(), evenals withLock die meer idiomatisch is.
    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) // Verwacht 10000
    }
    
  2. Semaphore

    • Beperkt het aantal coroutines dat gelijktijdig toegang kan krijgen tot een bron of een bepaald codeblok kan uitvoeren.
    • Beheert een permissie-pool.
    • Beschikt over functies acquire() en release(), evenals withPermit.
    import kotlinx.coroutines.sync.Semaphore
    import kotlinx.coroutines.sync.withPermit
    import kotlinx.coroutines.*
    
    val semaphore = Semaphore(2) // Maximaal 2 coroutines tegelijk
    
    suspend fun doLimitedWork(id: Int) {
        semaphore.withPermit {
            println("Coroutine $id heeft een permit verkregen. Werkt...")
            delay(100) // Simulatie van werk
            println("Coroutine $id heeft permit vrijgegeven.")
        }
    }
    
    suspend fun main() = coroutineScope {
        repeat(5) { i ->
            launch {
                doLimitedWork(i)
            }
        }
    }
    
  3. Atomische operaties (kotlinx.coroutines.atomic van)

    • Bieden thread-veilige operaties op primitieve types en referenties.
    • Gebruiken CPU-laag instructies (CAS - Compare-and-Swap).
    • Geschikt voor eenvoudige operaties zonder expliciete blokkades.
    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) // Verwacht 10000
    }
    
  4. Gedeelde datastructuren op een single-thread dispatcher

    • De eenvoudigste aanpak. Corrutines uitvoeren op een enkele thread-omgeving (Dispatchers.Default.limitedParallelism(1) of newSingleThreadContext) garandeert sequentiële uitvoering van de code, waardoor racecondities worden voorkomen. Het is geen expliciete synchronisatie-primitief, maar zorgt voor synchronisatie door toegang te serialiseren.
    import kotlinx.coroutines.*
    
    val mySingleThreadContext = newSingleThreadContext("SingleThread")
    var sharedData = mutableListOf<Int>()
    
    suspend fun addToSharedData(value: Int) {
        sharedData.add(value) // Veilig, omdat het op één thread wordt uitgevoerd
    }
    
    suspend fun main() = withContext(mySingleThreadContext) {
        repeat(10_000) {
            launch {
                 addToSharedData(it)
            }
        }
        println(sharedData.size) // Verwacht 10000
    }
    
  5. Kanalen

    • Zijn geen primitieve synchronisatie-instrumenten op zich, maar kunnen worden gebruikt voor veilige gegevensoverdracht tussen coroutines, wat indirect problemen met toegangssynchronisatie oplost.
    • Bieden een manier om datastromen van de ene naar de andere coroutine over te dragen.

De keuze van aanpak hangt af van het gebruiksscenario. Mutex en Semaphore bieden klassieke blokkeringmechanismen, atomische operaties zijn efficiënt voor eenvoudige atomaire updates, en de single-thread dispatcher is handig wanneer toegang tot gedeelde gegevens strikt sequentieel moet zijn. Kanalen worden gebruikt voor communicatie en coördinatie, niet voor directe bescherming van gedeelde bronnen.