Sobes.tech
Middle+

Wie kann man die Synchronisation in Kotlin mit Coroutinen implementieren?

sobes.tech KI

Antwort von AI

Die Synchronisierung in Kotlin-Coroutines wird mithilfe spezieller Primitive und Datenstrukturen aus der Bibliothek kotlinx.coroutines erreicht.

Hauptansätze:

  1. Mutex

    • Bietet gegenseitigen Ausschluss zum Schutz kritischer Codeabschnitte.
    • Die Coroutine, die versucht, ein Mutex zu erhalten, wird blockiert, bis es freigegeben wird.
    • Hat die Funktionen lock() und unlock(), sowie withLock als idiomatischere Variante.
    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) // Erwartet 10000
    }
    
  2. Semaphore

    • Begrenzung der Anzahl gleichzeitiger Koroutinen, die auf eine Ressource zugreifen oder einen bestimmten Codeblock ausführen können.
    • Verwalten eines Permissons-Pools.
    • Hat die Funktionen acquire() und release(), sowie withPermit.
    import kotlinx.coroutines.sync.Semaphore
    import kotlinx.coroutines.sync.withPermit
    import kotlinx.coroutines.*
    
    val semaphore = Semaphore(2) // Maximal 2 Koroutinen gleichzeitig
    
    suspend fun doLimitedWork(id: Int) {
        semaphore.withPermit {
            println("Koroutine $id hat eine Erlaubnis erhalten. Arbeitet...")
            delay(100) // Arbeit simulieren
            println("Koroutine $id hat Erlaubnis freigegeben.")
        }
    }
    
    suspend fun main() = coroutineScope {
        repeat(5) { i ->
            launch {
                doLimitedWork(i)
            }
        }
    }
    
  3. **Atomare Operationen (aus kotlinx.coroutines.atomic)

    • Bieten thread-sichere Operationen auf primitiven Typen und Referenzen.
    • Verwenden CPU-niedrigstufige Anweisungen (CAS - Compare-and-Swap).
    • Geeignet für einfache Operationen ohne explizite Sperren.
    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) // Erwartet 10000
    }
    
  4. Gemeinsame Datenstrukturen auf einem Single-Thread-Dispatcher

    • Der einfachste Ansatz. Ausführen von Koroutinen in einem Single-Thread-Kontext (Dispatchers.Default.limitedParallelism(1) oder newSingleThreadContext) garantiert sequentielle Ausführung, vermeidet Race Conditions. Keine explizite Synchronisationsprimitive, aber Serialisierung des Zugriffs.
    import kotlinx.coroutines.*
    
    val mySingleThreadContext = newSingleThreadContext("SingleThread")
    var sharedData = mutableListOf<Int>()
    
    suspend fun addToSharedData(value: Int) {
        sharedData.add(value) // Sicher, da auf einem einzigen Thread
    }
    
    suspend fun main() = withContext(mySingleThreadContext) {
        repeat(10_000) {
            launch {
                 addToSharedData(it)
            }
        }
        println(sharedData.size) // Erwartet 10000
    }
    
  5. Kanäle

    • Sind keine Synchronisationsprimitive an sich, können aber zum sicheren Datenaustausch zwischen Koroutinen verwendet werden, was indirekt Synchronisationsprobleme beim Zugriff auf die übertragenen Daten löst.
    • Bieten eine Möglichkeit, Datenströme von einer Koroutine zur anderen zu übertragen.

Die Wahl des Ansatzes hängt vom Anwendungsfall ab. Mutex und Semaphore bieten klassische Sperrmechanismen, atomare Operationen sind effizient für einfache atomare Updates, und der Single-Thread-Dispatcher ist praktisch, wenn der Zugriff auf gemeinsame Daten strikt sequentiell erfolgen soll. Kanäle werden für Kommunikation und Koordination genutzt, nicht für den direkten Schutz gemeinsamer Ressourcen.