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 :
-
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()etunlock(), ainsi quewithLockplus 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 } -
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()etrelease(), ainsi quewithPermit.
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) } } } -
**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 } -
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)ounewSingleThreadContext) 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 } - Approche la plus simple. Lancer des coroutines dans un contexte à thread unique (
-
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.