Sobes.tech
Middle+

Come si può implementare la sincronizzazione in Kotlin utilizzando le coroutine?

sobes.tech AI

Risposta dell'AI

La sincronizzazione nelle coroutine di Kotlin si ottiene utilizzando primitive e strutture dati speciali della libreria kotlinx.coroutines.

Approcci principali:

  1. Mutex

    • Fornisce esclusione mutua per proteggere le sezioni critiche del codice.
    • La coroutine che tenta di ottenere un Mutex si blocca fino a quando non viene rilasciato.
    • Ha funzioni lock() e unlock(), oltre a withLock più idiomatico.
    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) // Si aspetta 10000
    }
    
  2. Semaphore

    • Limita il numero di coroutine che possono accedere a una risorsa o eseguire un blocco di codice contemporaneamente.
    • Gestisce un pool di permessi.
    • Ha funzioni acquire() e release(), oltre a withPermit.
    import kotlinx.coroutines.sync.Semaphore
    import kotlinx.coroutines.sync.withPermit
    import kotlinx.coroutines.*
    
    val semaphore = Semaphore(2) // Fino a 2 coroutine contemporaneamente
    
    suspend fun doLimitedWork(id: Int) {
        semaphore.withPermit {
            println("Coroutine $id acquisito un permesso. Lavorando...")
            delay(100) // Simulazione di lavoro
            println("Coroutine $id rilasciato un permesso.")
        }
    }
    
    suspend fun main() = coroutineScope {
        repeat(5) { i ->
            launch {
                doLimitedWork(i)
            }
        }
    }
    
  3. **Operazioni atomiche (da kotlinx.coroutines.atomic)

    • Offrono operazioni thread-safe su tipi primitivi e riferimenti.
    • Utilizzano istruzioni CPU di basso livello (CAS - Compare-and-Swap).
    • Adatte per operazioni semplici senza blocchi espliciti.
    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) // Si aspetta 10000
    }
    
  4. Strutture dati condivise su un dispatcher a thread singolo

    • Approccio più semplice. Eseguire coroutine in un contesto a thread singolo (Dispatchers.Default.limitedParallelism(1) o newSingleThreadContext) garantisce l'esecuzione sequenziale del codice, evitando condizioni di gara. Non è una primitive di sincronizzazione esplicita, ma garantisce la sincronizzazione serializzando l'accesso.
    import kotlinx.coroutines.*
    
    val mySingleThreadContext = newSingleThreadContext("SingleThread")
    var sharedData = mutableListOf<Int>()
    
    suspend fun addToSharedData(value: Int) {
        sharedData.add(value) // Sicuro, poiché eseguito su un solo thread
    }
    
    suspend fun main() = withContext(mySingleThreadContext) {
        repeat(10_000) {
            launch {
                 addToSharedData(it)
            }
        }
        println(sharedData.size) // Si aspetta 10000
    }
    
  5. Canali

    • Non sono primitive di sincronizzazione in sé, ma possono essere usati per trasferire dati in modo sicuro tra coroutine, risolvendo indirettamente problemi di sincronizzazione dell'accesso ai dati trasmessi.
    • Forniscono un modo per trasferire flussi di dati da una coroutine all'altra.

La scelta dell'approccio dipende dallo scenario di utilizzo. Mutex e Semaphore forniscono meccanismi classici di blocco, le operazioni atomiche sono efficienti per aggiornamenti semplici, e il dispatcher a thread singolo è conveniente quando l'accesso ai dati condivisi deve essere strettamente sequenziale. I canali sono usati per comunicazione e coordinamento, non per proteggere direttamente le risorse condivise.