CodeGym /Kurslar /Kotlin SELF /Confinement ve Channel vasitesile actor: veziyyetin tek s...

Confinement ve Channel vasitesile actor: veziyyetin tek sahibi

Kotlin SELF
Səviyyə, Dərs
Mövcuddur

1. Giris

Siz konkurent kod yazanda beyin cox tez “men indi her seyi diqqetle sinxronlasdiracam, dogrudan-dograya” rejimine kecir. Amma realliq amansizdir: kodda eyni var-i deyisen yerler nə qeder coxdursa, nadir, narahatedici ve “menim noutbukumda tekrarlanmır” tipli bag-i tutmaq ehtimali bir o qeder artir. Ona gore bu gun biz elə bir yanaşmadan danisacagıq ki, yarislari (race) “temir etmir”, əksinə elə edir ki, yarislər konstruksiya uzre umumen olmasin.

Korinlerde umumi deyisdirile bilən veziyyet (shared mutable state) problemi bir neche korutinin eyni melumatlari koordinasiya olmadan oxuyub yazmasi zamani yaranir — ve onda asanliqla race condition peyda olur. Bunu Mutex ve atomiklerle hell etmek olar, amma ucuncu yol da var: umumen veziyyeti bolusmemek.

Confinement: “menim var-imdan el cekin”

Confinement (rusca tez-tez “izolyasiya olunmus veziyyet” ve ya “veziyyet bir yerde” de deyirler) — bu qaydadir: melumatlari yalniz bir sahibi deyisir, qalanlar ise sahib ile “danisir”. Bu, bele bir veziyyete benzer: umumi metbexe yalniz bir asbaz buraxilir, qalanlar ona qeydiyyat yazarlar: “duz elave et”, “cay demle”, “pechenye ne qeder qalib?”. Asbaz oz-ozu ile munaqise etmir ve tava ugrunda davaya girmir — cunki o tektir.

Praktikada confinement dusunceni cox guclu sekilde sadelestirir: siz konkret bir deyisen ucun “axinlarin/korutinlerin butun mumkun qarisiqlari”ni artiq nezerden kecirmirsiniz. Siz mesajlarin ardicil emalini nezerden kecirmeye baslayirsiniz. Ardicilliq ise proqramcilarin adeten en yaxsi bacardigi seyidir (axı biz kursa println ile baslamamisiq ki).

Vacibdir: confinement yalniz o zaman isleyir ki, qayda dogrudan da yerinə yetirilsin. Eger siz kiməsə “bir deqiqelik” veziyyete birbasa giris verseniz, siz yeniden race-dunya-sina qayidacaqsiniz, amma indi onlar daha da hiyləkər olacaq, cunki deyisiklerin bir hissesi mesaj protokolu ile gedir, bir hissesi ise “kassani dolanaraq”.

2. Actor-yanaşmasi: veziyyetin sahibi ve Channel

Indi ideyani koda yaxinlasdiraq. Actor-yanaşmasi confinement-in cox praktik reallasdirilmasidir: bizde veziyyete sahib olan bir korutina var ve Channel var, diger korutinler onunla mesaj gonderir. Burada Channel novbe rolu oynayir: gonderenler send(...) edir, sahib receive edir (adeten for dongusu ile), mesaji emal edir ve oz lokal veziyyetini yenileyir.

Channel-in esasy modelini xatirladim: bu, korutinler arasinda kommunikasiya usuludur, burada her mesaj deqiq olaraq bir qebul ediciye catdirilir. Bu, “tek sahib” ideyasi ile ideal sekilde ust-uste dusur: biz isteyirik ki, veziyyetin butun deyisiklikleri tek noqte uzerinden kecsin.

Beyin ucun daha asan olsun deye sxem cizek (yalniz kompilyator ucun yox):

flowchart LR
    subgraph Workers["Diger korutinler (istehsalchilar)"]
        W1["worker #1"]
        W2["worker #2"]
        W3["worker #3"]
    end

    C["Channel⟨Msg⟩ (poct qutusu)"]

    subgraph Owner["Actor (sahib-korutina)"]
        S["State (yalniz burda!)"]
        L["for (msg in channel) { ... }"]
    end

    W1 -->|"send(msg)"| C
    W2 -->|"send(msg)"| C
    W3 -->|"send(msg)"| C
    C -->|"receive(msg)"| L
    L -->|"deyisir"| S

Burada vacib fikir: veziyyet umumen worker-ler ucun gorunmur. Onlar yalniz kanali, yəni “mesaj API”-sini gorurler. Bu, tesadufen sehv bir sey etmə ehtimalini kəskin azaldir.

4. Minimal actor: race olmadan saygac

Dərhal “boyuk proqram”dan baslamamaq ucun mexanikanı gosterən kicik numune yigaq. Actor counter-i saxlayacaq, xarici korutinler ise ona “1 elave et” gonderecek.

import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val mailbox = Channel<Int>() // сообщение = delta

    val owner = launch {
        var counter = 0
        for (delta in mailbox) counter += delta
        println("final counter=$counter") // final counter=1000
    }

    repeat(1_000) { launch { mailbox.send(1) } }
    mailbox.close()
    owner.join()
}

Burada gormek lazim olan: counter — adi var-dir, nə Mutex var, nə də AtomicInteger. Buna baxmayaraq her sey dogrudur, cunki ona sahibden basqa hech kim toxunmur. Bizde shared mutable state yoxdur — bizde “private state + message passing” var.

5. Tiplesdirilmis mesajlar ve protokol

Real heyatda “sadəcə reqem” kimi mesajlar cox tez qarisiqliga cevrilir: 1 nə deməkdir? “bir elave et”? “sehv kodu”? “pisiklerin sayi”? Ona gore novbeti addim mesajlari asikar ve tiplesdirilmis etmekdir. Kotlin-de bunun ucun en rahat alət — sealed class-dir.

Ideya sadedir: Channel<Int> əvəzinə Channel<Msg> edirik, burada Msg — sahibimiz ucun komandalar toplusudur.

import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

sealed class Msg
data class Add(val value: Int) : Msg()
object Stop : Msg()

fun main() = runBlocking {
    val mailbox = Channel<Msg>()

    val owner = launch {
        var sum = 0
        for (msg in mailbox) when (msg) {
            is Add -> sum += msg.value
            Stop -> break
        }
        println("sum=$sum") // sum=15
    }

    mailbox.send(Add(10))
    mailbox.send(Add(5))
    mailbox.send(Stop)
    owner.join()
}

Bu niye “sadəcə reqemler”den daha gucludur? Cunki siz senedlesdirilmis protokol alirsiniz: Add komandasi var, Stop komandasi var, ve kompilyator size “delta”ni “dayanma komandasi” gozlenilen yere gondermemeye komek edir.

6. Numune: xerc actor-saxlayicisi

Təsəvvur edək ki, kurs boyunca bizde “Budget Tracker” adlı konsol proqramı boyuyub: xərcləri saxlayırıq, onları elave edirik ve cemini hesablayırıq. Indiye qeder her sey sadə idi: bir icra axini, bir giris, bir kolleksiya. Amma indi tutaq ki, xərclər muxtelif menbelerden gelir: bir worker əməliyyatlari “import edir”, digeri — istifadecini simulyasiya edir, ucuncu — dovri silinmeleri elave edir. Eger hamisi bir MutableList-i deyisdirse, pis olacaq.

Ona gore biz veziyyetin sahibini — “xərc saxlayicisi”ni — yaradacagıq ve onunla mesajlarla elaqe saxlayacagıq.

Melumat modelinden baslayaq:

data class Expense(
    val id: Int,
    val title: String,
    val amount: Int
)

Indi mesaj protokolu. Biz “xərc elave et”, “id uzre sil”, “dayan” isteyirik. Diqqet edin: hələ ki, geriye dogru murəkkəb cavablar etmirik, ona gore komandalar “biristiqametli” olacaq.

sealed class BudgetMsg
data class AddExpense(val expense: Expense) : BudgetMsg()
data class RemoveExpense(val id: Int) : BudgetMsg()
object StopBudget : BudgetMsg()

Xərclərin siyahisini saxlayan sahib (actor) yigaraq:

import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val mailbox = Channel<BudgetMsg>(Channel.BUFFERED)

    val owner = launch {
        val expenses = mutableListOf<Expense>()
        for (msg in mailbox) when (msg) {
            is AddExpense -> expenses.add(msg.expense)
            is RemoveExpense -> expenses.removeIf { it.id == msg.id }
            StopBudget -> break
        }
        println("stopped, total items=${expenses.size}")
    }

    mailbox.send(AddExpense(Expense(1, "Coffee", 250)))
    mailbox.send(AddExpense(Expense(2, "Pizza", 900)))
    mailbox.send(RemoveExpense(1))
    mailbox.send(StopBudget)

    owner.join()
}

Burada esas sey bas verdi: expenses-e xarıcden umumen “tesadufen toxunmaq” mumkun deyil. Xarici kod expenses.add(...) veya expenses.clear() ede bilmir — cun ki onun elinde hec bir istinad yoxdur. Bu, confinement-in emelde isleməsidir: veziyyete giris fizik olaraq sahibin gorunus sahəsi ile mehdudlasdirilir.

7. Bir neche gonderen: konkurentliyin imitasiya olunmasi

Indi butun bunlar niye lazım idi — onu elave edək: eyni anda komandalar gonderən bir neche korutina. Bu, shared mutable state-in sizi demək olar ki, qarantili sekilde tutdugu andir, amma actor sakitce yasamağa davam edir.

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val mailbox = Channel<BudgetMsg>(Channel.BUFFERED)

    val owner = launch {
        var total = 0
        for (msg in mailbox) when (msg) {
            is AddExpense -> total += msg.expense.amount
            StopBudget -> break
            else -> Unit
        }
        println("total=$total") // например: total=3000
    }

    val w1 = launch(Dispatchers.Default) { mailbox.send(AddExpense(Expense(1, "A", 1000))) }
    val w2 = launch(Dispatchers.Default) { mailbox.send(AddExpense(Expense(2, "B", 1000))) }
    val w3 = launch(Dispatchers.Default) { mailbox.send(AddExpense(Expense(3, "C", 1000))) }

    w1.join(); w2.join(); w3.join()
    mailbox.send(StopBudget)
    owner.join()
}

Diqqet edin: mesajlarin gelis sirasi istenilen kimi ola biler, amma yekun cem dogru olacaq, cun ki sahibde total-a “paralel yazilislar” yoxdur. O, mesajlari ardicil emal edir.

8. Melumati geri nece almaq olar: sorgu ve cavab

Proqram bir az daha realist olduqda sual yaranir: “Yaxsi, biz komandalar gondere bilirik… bəs cari cemi nece bilek?” Ve burda cox asanliqla pis dizayna surusmek olur: “gelin sadəcə xarıcə expenses və ya total verək, heç nə olmaz”. Bu, məhz confinement-i sındırdığınız andır.

Duzgun addim — melumati sahibden mesajla istəmək ve cavabi ayri cavab kanali uzerinden almaqdir. Bəli, bu “kanaldan kanała” kimi səslənir, amma actor-lar ucun bu tam normal texnikadir.

Gəlin GetTotal mesajini edək: o, “cavabi hara gondermek” melumatini dasiyir:

import kotlinx.coroutines.channels.SendChannel

sealed class BudgetMsg
data class AddExpense(val expense: Expense) : BudgetMsg()
data class GetTotal(val replyTo: SendChannel<Int>) : BudgetMsg()
object StopBudget : BudgetMsg()

Indi sahib cavab vere bilir:

import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val mailbox = Channel<BudgetMsg>(Channel.BUFFERED)

    val owner = launch {
        var total = 0
        for (msg in mailbox) when (msg) {
            is AddExpense -> total += msg.expense.amount
            is GetTotal -> msg.replyTo.send(total)
            StopBudget -> break
        }
    }

    mailbox.send(AddExpense(Expense(1, "Coffee", 250)))

    val reply = Channel<Int>(capacity = 1)
    mailbox.send(GetTotal(reply))
    println("total=${reply.receive()}") // total=250

    mailbox.send(StopBudget)
    owner.join()
}

Acar ideya: xarici kod hələ də total-i görmür, amma sahibə sual verib cavabi tehlukesiz formada ala bilir. Bu, destək xidmətinə sorğuya bənzəyir: siz onların verilənlər bazasına girmirsiniz, siz tiket açırsınız və cavab alırsınız.

9. Heyat dongusu: actor-u duzgun nece dayandirmaq olar

Siz ilk defe actor yazanda cox isterdiniz ki, “qoy o proqram yasadigi muddetce yaşasın”. Sonra real dayandirma ssenarisini elave edirsiniz — ve birdən anlayirsiniz ki, protokolun bitmesi başlamaq qədər vacibdir. Eger actor-u dayandirmasaniz, o resurslari (yaddas, fayl deskriptorlari, birlesmeler) saxlaya biler; eger sehv dayandirsaniz — gonderenler send edende sehvlerle dusməyə baslayacaq.

Dayandirma ucun iki əsas uslub var; istənilənini secə bilərsiniz, təki ardicil ve razilasdirilmis olsun.

Birinci uslub: xususi StopBudget mesajini gondermek, emal edici break edir, sonra sahib dongunu duzgun bitirir. Bu ona gore rahatdir ki, dayandirma protokolun hissəsidir.

Ikinci uslub: kanali baglamaq — mailbox.close(). Onda for dongusu mesajlar bitəndə avtomatik bitəcək. Bu ona gore rahatdir ki, “kanal resurs kimi” xarıcden aciq sekilde baglanir.

Tədris numunelerinde adeten Stop istifadə etmək daha asandır, cunki daha aydindir. Amma vacibdir: uslublari səbəbsiz qarisdirmayin. Eger razilasib “kanali baglayiriq” demisinizse, onda əlavə olaraq Stop gonderməyin; cunki onu tesadufen artıq bagli kanala gondere bilersiniz (ve istisna alarsiniz), ve ya sahib yigilmis mesajlar emal olunmamışdan əvvəl dayanar.

10. Actor vs Mutex: qisa muqayise

Bir neche numuneden sonra adeten sual yaranir: “Bəs onda niyə Mutex lazimdir, actor varkən?” Cavab: hər iki alət faydalıdır, amma fərqli dusunme tərzini ifadə edir.

Mutex ile sizde shared mutable state qalir, sadəcə kritik seksiyaya girisde “slaqbaum” qoyursunuz. Bu, kiçik bir kod hissəsini tez qorumaq lazım olanda ve sərhədlərə dəqiq nəzarət etdiyinizdə rahat olur.

Actor ile siz arxitekturanı dəyişirsiniz: siz verilənlərə girisi qorumursunuz, girisi aradan qaldirirsiniz. Sizde mesaj novbesi ve aciq komanda protokolu yaranir. Bazen bu bir az daha cox kod deməkdir, amma çox vaxt xətalar radikal dərəcədə az olur, cunki siz “butun proqramcilar her zaman hansi Mutex-i goturməyi xatirlayacaq” fikrinə daha az güvənirsiniz.

Beynin bunu iki rəfə yığması üçün kiçik cədvəl:

Yanasma Veziyyetle nə bas verir Tehlukesizlik nece əldə olunur Adeten harada rahatdir
Mutex
Veziyyet umumi, amma giris “novbe ile” Kritik seksiyanin bloklanmasi Mövcud kodun uzerine kicik duzelisler
Actor / confinement
Veziyyet umumen bolusulmur Tek sahib + mesajlar Merkezlesdirilmis veziyyet (saygac, balans, tapşiriq novbesi, “saxlayici”)

Bir də vacib nuance: actor təbii olaraq “dusunmeni miqyaslandirir”. Yeni komanda yarandıqda, siz yeni mesaj tipini və bir yerde yeni when budagini elave edirsiniz. Bu idealı qarantilemir, amma sistemi başda saxlamağa ciddi komek edir.

Antipattern: actor var, amma veziyyet xarıcə “sızıb”

Indi bir az agri olacaq, cunki bu, yeni baslayanlarin cox tipik sehvdir. Adam vicdanla actor edir, sonra düşünür: “Axı men xərclərin siyahisini hardasa gostermeliyəm… men sadəcə MutableList-i qaytararam, hec nə olmaz”. Və məhz burda confinement bitir.

Pis ideya təxminən bele gorunur (numune qəsdən “məna uzre”dir, bele etməyin):

// BAD: a reference to a mutable list leaks outside.
// Then anyone can do expenses.add(...) in parallel with the actor.
fun leakState(expenses: MutableList<Expense>): MutableList<Expense> = expenses

Duzgun uslub: eger “siyahini gostermek” lazimdirsa, actor-a sorgu gonderin ve ya siyahinin kopyasini qaytarin, ya da hazir hesabat setrini qaytarin. Yəni xarıcə netice cixmalidir, yoxsa “daxili hissələri idarə etmə dəstəyi”.

11. Channel uzerinden confinement/actor ile işleyərkən tipik sehvler

Sehv №1: veziyyete birbasa girisi “her ehtimala qarsi” saxladilar.
Cox vaxt bu gunahsız gorunur: qlobal var total, ustəlik actor da “güyə total hesablayir”. Sonra kodun bir hissesi tesadufen total-i birbasa yenileyir, digəri — mesajlarla, ve siz yenə shared mutable state alirsiniz. Confinement yalniz sərt qayda ile işleyir: veziyyeti tek sahib dəyişir, nöqtə.

Sehv №2: mesajlar tiplesdirilməyib ve “sehirli reqemler/setrler”e cevrilir.
Channel<Any> ve ya Channel<String> icerisinde "ADD 10" ve "STOP" kimi komandalar ucmaga baslayanda, siz ozunuz ucun mini-protokol parseri yazirsiniz ve formatlarin dogrulugunu elle saxlayirsiniz. sealed class ve when ile protokol kompilyator terefinden yoxlanilir, ve bu, kompilyatorun “darixdirici” yox, dogrudan da xilas edici oldugu nadir hallardandir.

Sehv №3: dayandirma uzre razilasma yoxdur ve kanal hec vaxt baglanmir.
Eger sahib for dongusunde oturubsa, siz ise bir defe də Stop gondermursunuz ve close() etmirsinizse, o sonsuza qeder gozleyecek. Konsol proqraminda bu, cox vaxt “her seyi etdi, amma proqram sonda dondu” deməkdir. Evvelden qerar vermek lazimdir: dayandirmaya kim cavabdehdir ve hansi usulla.

Sehv №4: gonderenler hələ işləyərkən kanali cox tez bagladilar.
Eger “esas korutina” mailbox-i baglayibsa, amma worker-ler hələ send etmeye calisirlarsa, siz istisnalar və mesaj itkiləri alacaqsiniz. Evvelce gonderenleri gozlemek lazimdir (join()), yalniz sonra actor-u bitirmek. Bəli, bu darixdirici səslənir. Amma işləyir.

Sehv №5: sahibin daxilinde agir is actor-u “dar bogaz”a cevirir.
Actor mesajlari ardicil emal edir — bu onun supergucu ve onun məhdudiyyətidir. Eger when daxilinde uzun bir is gorursunuzse (meselen, bloklayan I/O ve ya agir hesablamalar), novbe boyuyəcək ve sistem yavaslayacaq. Yaxsi uslub — mesaj emalini qisa saxlamaqdir: veziyyeti yenilemek, faktı qeyd etmek, agir isi ise ele teskil etmek ki, sahib sonsuz əzabkeşe cevrilməsin.

Sehv №6: sorgu/cavab əvəzinə istinad sızdıraraq “melumat almağa” calisirlər.
Ən cazibədar yol — daxili siyahıya veya veziyyet obyektine istinadi xarıcə qaytarmaqdir. Amma bu, confinement-in mənasını tamamilə məhv edir. Eger melumati xarıcə vermək lazimdirsa, tehlukesiz formada verin: kopya, reqem, hesabat setri ve ya ayri reply-kanal uzerinden cavab.

Sehv №7: ehtiyac olmadan muxtelif protokol uslublarini qarisdirirlar.
Meselen, bazen kanali close() edirler, bazen Stop gonderirler, bazen de her ikisini tesadufi sirada edirlər. Neticede sahib novbeni emal etmədən əvvəl bitə bilər, ve ya gonderen artıq bagli kanala Stop gondermeye calisa biler. Protokol tek, sade ve proqnozlasdirila bilən olmalidir — onda debug da insan kimi olacaq.

1
Tapşırıq
Kotlin SELF, səviyyə, dərs
Bağlanıb
Poçt qutusu
Poçt qutusu
1
Tapşırıq
Kotlin SELF, səviyyə, dərs
Bağlanıb
Toplayıcının komandaları
Toplayıcının komandaları
1
Tapşırıq
Kotlin SELF, səviyyə, dərs
Bağlanıb
Üç kuryer
Üç kuryer
1
Tapşırıq
Kotlin SELF, səviyyə, dərs
Bağlanıb
Sorğu ilə balans
Sorğu ilə balans
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION