Sobes.tech
Middle+

Comment peut-on implémenter la synchronisation en Kotlin en utilisant des coroutines?

sobes.tech IA

Réponse de l'IA

La synchronisation dans les coroutines Kotlin est réalisée à l'aide de primitives et de structures de données spéciales de la bibliothèque kotlinx.coroutines.

Approches principales :

  1. Mutex

    • Fournit une exclusion mutuelle pour protéger les sections critiques du code.
    • La coroutine qui tente d'obtenir un Mutex se bloque jusqu'à ce qu'il soit libéré.
    • Dispose des fonctions lock() et unlock(), ainsi que withLock plus idiomatique.
    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) // On attend 10000
    }
    
  2. Semaphore

    • Limite le nombre de coroutines pouvant accéder simultanément à une ressource ou exécuter un bloc de code.
    • Gère un pool de permissions.
    • Dispose des fonctions acquire() et release(), ainsi que withPermit.
    import kotlinx.coroutines.sync.Semaphore
    import kotlinx.coroutines.sync.withPermit
    import kotlinx.coroutines.*
    
    val semaphore = Semaphore(2) // Jusqu'à 2 coroutines en même temps
    
    suspend fun doLimitedWork(id: Int) {
        semaphore.withPermit {
            println("Coroutine $id a obtenu un permis. Travaille...")
            delay(100) // Simulation de travail
            println("Coroutine $id a libéré un permis.")
        }
    }
    
    suspend fun main() = coroutineScope {
        repeat(5) { i ->
            launch {
                doLimitedWork(i)
            }
        }
    }
    
  3. **Opérations atomiques (de kotlinx.coroutines.atomic)

    • Offrent des opérations sûres pour les threads sur des types primitifs et des références.
    • Utilisent des instructions CPU de bas niveau (CAS - Compare-and-Swap).
    • Adaptées pour des opérations simples sans verrouillages explicites.
    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) // On attend 10000
    }
    
  4. Structures de données partagées sur un dispatcher mono-thread

    • Approche la plus simple. Lancer des coroutines dans un contexte à thread unique (Dispatchers.Default.limitedParallelism(1) ou newSingleThreadContext) garantit une exécution séquentielle du code, évitant les conditions de course. Ce n'est pas une primitive de synchronisation explicite, mais cela assure la synchronisation en sérialisant l'accès.
    import kotlinx.coroutines.*
    
    val mySingleThreadContext = newSingleThreadContext("SingleThread")
    var sharedData = mutableListOf<Int>()
    
    suspend fun addToSharedData(value: Int) {
        sharedData.add(value) // Sûr, car exécuté sur un seul thread
    }
    
    suspend fun main() = withContext(mySingleThreadContext) {
        repeat(10_000) {
            launch {
                 addToSharedData(it)
            }
        }
        println(sharedData.size) // On attend 10000
    }
    
  5. Canaux

    • Ne sont pas des primitives de synchronisation en soi, mais peuvent être utilisés pour transférer des données en toute sécurité entre coroutines, ce qui résout indirectement les problèmes de synchronisation d'accès aux données transmises.
    • Fournissent un moyen de transférer un flux de données d'une coroutine à une autre.

Le choix de l'approche dépend du scénario d'utilisation. Mutex et Semaphore offrent des mécanismes classiques de verrouillage, les opérations atomiques sont efficaces pour des mises à jour simples, et le dispatcher mono-thread est pratique lorsque l'accès aux données partagées doit être strictement séquentiel. Les canaux sont utilisés pour la communication et la coordination, et non pour la protection directe des ressources partagées.