Sobes.tech
Middle+

Kako implementirati sinhronizaciju u Kotlinu koristeći korutine?

sobes.tech АИ

Одговор од АИ

Sinhronizacija u Kotlin korutinama se postiže pomoću specijalnih primitiva i struktura podataka iz biblioteke kotlinx.coroutines.

Glavni pristupi:

  1. Mutex

    • Obezbeđuje međusobno isključivanje za zaštitu kritičnih sekcija koda.
    • Korutina koja pokuša da dobije Mutex blokira se dok se on ne oslobodi.
    • Ima funkcije lock() i unlock(), kao i idiomatski withLock.
    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) // Očekujemo 10000
    }
    
  2. Semaphore

    • Ograničava broj korutina koje mogu istovremeno da pristupe resursu ili izvrše određeni blok koda.
    • Upravlja skupom dozvola.
    • Ima funkcije acquire() i release(), kao i withPermit.
    import kotlinx.coroutines.sync.Semaphore
    import kotlinx.coroutines.sync.withPermit
    import kotlinx.coroutines.*
    
    val semaphore = Semaphore(2) // Istovremeno mogu raditi 2 korutine
    
    suspend fun doLimitedWork(id: Int) {
        semaphore.withPermit {
            println("Korutina $id je dobila dozvolu. Radi...")
            delay(100) // Imitacija rada
            println("Korutina $id je oslobodila dozvolu.")
        }
    }
    
    suspend fun main() = coroutineScope {
        repeat(5) { i ->
            launch {
                doLimitedWork(i)
            }
        }
    }
    
  3. Atomicne operacije (iz kotlinx.coroutines.atomic)

    • Obezbeđuju thread-safe operacije nad primitivnim tipovima i referencama.
    • Koriste niskonivojske CPU instrukcije (CAS - Compare-and-Swap).
    • Pogodne za jednostavne operacije bez eksplicitnih blokada.
    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) // Očekujemo 10000
    }
    
  4. Deljene strukture podataka na dispatcheru sa jednim nitom

    • Najjednostavniji pristup. Pokretanje korutina na kontekstu sa jednim nitom (Dispatchers.Default.limitedParallelism(1) ili newSingleThreadContext) garantuje sekvencijalno izvršavanje koda, izbegavajući trke podataka. Nije to eksplicitni primitivan za sinhronizaciju, ali obezbeđuje sinhronizaciju putem serializacije pristupa.
    import kotlinx.coroutines.*
    
    val mySingleThreadContext = newSingleThreadContext("SingleThread")
    var sharedData = mutableListOf<Int>()
    
    suspend fun addToSharedData(value: Int) {
        sharedData.add(value) // Sigurno, jer se izvršava na jednom thread-u
    }
    
    suspend fun main() = withContext(mySingleThreadContext) {
        repeat(10_000) {
            launch {
                 addToSharedData(it)
            }
        }
        println(sharedData.size) // Očekujemo 10000
    }
    
  5. Kanali

    • Nisu primitives za sinhronizaciju u čistom obliku, ali se mogu koristiti za sigurnu transmisiju podataka između korutina, što indirektno rešava probleme sinhronizacije pristupa podacima.
    • Omogućavaju prenos toka podataka iz jedne korutine u drugu.

Izbor pristupa zavisi od scenarija korišćenja. Mutex i Semaphore pružaju klasične mehanizme blokiranja, atomarne operacije su efikasne za jednostavne atomarne ažuriranja, a dispatcher sa jednim thread-om je zgodan kada pristup zajedničkim podacima mora biti strogo sekvencijalni. Kanali se koriste za komunikaciju i koordinaciju, a ne za direktnu zaštitu zajedničkih resursa.