Sobes.tech
Middle+

¿Cómo se puede implementar la sincronización en Kotlin usando corutinas?

sobes.tech AI

Respuesta de la IA

La sincronización en las corrutinas de Kotlin se logra mediante primitivas y estructuras de datos especiales de la biblioteca kotlinx.coroutines.

Enfoques principales:

  1. Mutex

    • Proporciona exclusión mutua para proteger secciones críticas del código.
    • La corrutina que intenta obtener un Mutex se bloquea hasta que se libera.
    • Tiene funciones lock() y unlock(), así como withLock más idiomático.
    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) // Se espera 10000
    }
    
  2. Semaphore

    • Limita la cantidad de corrutinas que pueden acceder a un recurso o ejecutar un bloque de código simultáneamente.
    • Gestiona un grupo de permisos.
    • Tiene funciones acquire() y release(), así como withPermit.
    import kotlinx.coroutines.sync.Semaphore
    import kotlinx.coroutines.sync.withPermit
    import kotlinx.coroutines.*
    
    val semaphore = Semaphore(2) // Hasta 2 corrutinas simultáneamente
    
    suspend fun doLimitedWork(id: Int) {
        semaphore.withPermit {
            println("Corrutina $id adquirió un permiso. Trabajando...")
            delay(100) // Simulación de trabajo
            println("Corrutina $id liberó un permiso.")
        }
    }
    
    suspend fun main() = coroutineScope {
        repeat(5) { i ->
            launch {
                doLimitedWork(i)
            }
        }
    }
    
  3. **Operaciones atómicas (de kotlinx.coroutines.atomic)

    • Ofrecen operaciones seguras para hilos sobre tipos primitivos y referencias.
    • Utilizan instrucciones de CPU de bajo nivel (CAS - Compare-and-Swap).
    • Adecuadas para operaciones simples sin bloqueos explícitos.
    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) // Se espera 10000
    }
    
  4. Estructuras de datos compartidas en un despachador de un solo hilo

    • El enfoque más simple. Ejecutar corrutinas en un contexto de un solo hilo (Dispatchers.Default.limitedParallelism(1) o newSingleThreadContext) garantiza la ejecución secuencial del código, evitando condiciones de carrera. No es una primitiva de sincronización explícita, pero asegura sincronización mediante serialización del acceso.
    import kotlinx.coroutines.*
    
    val mySingleThreadContext = newSingleThreadContext("SingleThread")
    var sharedData = mutableListOf<Int>()
    
    suspend fun addToSharedData(value: Int) {
        sharedData.add(value) // Seguro, ya que se ejecuta en un solo hilo
    }
    
    suspend fun main() = withContext(mySingleThreadContext) {
        repeat(10_000) {
            launch {
                 addToSharedData(it)
            }
        }
        println(sharedData.size) // Se espera 10000
    }
    
  5. Canales

    • No son primitivas de sincronización en sí mismas, pero pueden usarse para transferir datos de forma segura entre corrutinas, lo que resuelve indirectamente problemas de sincronización de acceso a los datos transmitidos.
    • Proporcionan un método para transferir un flujo de datos de una corrutina a otra.

La elección del enfoque depende del escenario de uso. Mutex y Semaphore ofrecen mecanismos clásicos de bloqueo, las operaciones atómicas son eficientes para actualizaciones simples, y el despachador de un solo hilo es conveniente cuando el acceso a datos compartidos debe ser estrictamente secuencial. Los canales se usan para comunicación y coordinación, no para protección directa de recursos compartidos.