About Blog Projects

Golang Channels

Golang Channels

Channel nima?

Go dasturlash tilining eng kuchli xususiyatlaridan biri bu — goroutinlar o'rtasida muloqotni ta'minlovchi "channel" mexanizmidir.
Ko'pgina dasturlash tillari parallel ishlov berish uchun umumiy xotira (Shared Memory) modelini qo'llaydi. Go esa boshqacha yo'l tutadi: CSP (Communicating Sequential Processes) modelini amalga oshiradi.
Go'ning asosiy falsafasi: 

Do not communicate by sharing memory; instead, share memory by communicating.

Channel yaratish va ishlatish

Channel'ni make() funksiyasi orqali yaratasiz:

// Buferlanmagan channel (int turi)
ch := make(chan int)
// Buferli channel (3 ta joy)
ch := make(chan int, 3)
// String uchun channel
ch := make(chan string)
// Bo'sh signal channel (faqat signal uchun)
ch := make(chan struct{})

Channel yuborish bilan ishlashda faqat bitta operatsiya belgisi ishlatiladi: <-

ch <- 42        // channel'ga 42 yuborish (arrow channelga qaragan)
val := <-ch     // channel'dan qabul qilish (arrow channeldan chiqayapti)
<-ch            // qabul qilish, lekin qiymatni saqlamaslik
func main() {
   ch := make(chan int)  // channel yaratildi

   // goroutine orqali yuborish
   go func() {
       ch <- 42  // 42 ni kanalga yuboradi
   }()

   // qabul qilish (bloklanib kutadi)
   val := <-ch
   fmt.Println(val)  // Natija: 42
}

Bu yerda main goroutine val := <-ch qatorida to'xtab, goroutine ma'lumot yuborguncha kutadi. Bu channel'ning sinxronizatsiya kuchi!
 

Unbuffered Channel

Buferlanmagan channel — ikki goroutine bir vaqtda tayyor bo'lgandagina ishlaydi. Biri ma'lumot yuborsa, ikkinchisi tayyor bo'lguncha birinchisi kutadi.

Buferlanmagan channel'da sinxron kutish

Yuboruvchi va qabul qiluvchi bir vaqtda tayyor bo'lishi shart. Aks holda birinchi tayyori ikkinchisini kutadi.

func main() {
    ch := make(chan int)  // buferlanmagan
 
    go func() {
        fmt.Println("Yuborishdan oldin...")
        ch <- 1  // qabul qiluvchi tayyor bo'lguncha kutadi
        fmt.Println("Yuborishdan keyin (blok tugadi)")
    }()
 
    time.Sleep(2 * time.Second)  // 2 soniya kech turing
    fmt.Println("Qabul qilishga tayyor")
    val := <-ch  // endi qabul qilindi
    fmt.Println("Qabul qilindi:", val)
    time.Sleep(100 * time.Millisecond)
}

Buffered Channel


Buferli channel'da ma'lumotlar vaqtincha saqlanadi. Bufer to'lgunga qadar yuboruvchi bloklanmaydi.

Buferli channel — vaqtincha saqlash joyi bor
ch := make(chan int, 3)  // 3 ta joy bor
 
ch <- 42  // joy bor, bloklanmaydi
ch <- 17  // joy bor, bloklanmaydi
ch <- 99  // joy bor, bloklanmaydi
// ch <- 100  // XATO! Bufer to'ldi, bloklanadi
 
val1 := <-ch  // 42 (FIFO tartibida)
val2 := <-ch  // 17
val3 := <-ch  // 99
fmt.Println(val1, val2, val3)  // 42 17 99

Channel'ni yopish

Channel'ni yopish orqali "endi ma'lumot kelmaydi" signali yuboriladi. Bu ayniqsa for range bilan channel'dan o'qishda muhim.

Yopish va tekshirish

ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)  // channel yopildi

// ok tekshiruvi
val, ok := <-ch
fmt.Println(val, ok)  // 1 true  (qiymat bor)
val, ok = <-ch
fmt.Println(val, ok)  // 2 true
val, ok = <-ch
fmt.Println(val, ok)  // 3 true
val, ok = <-ch
fmt.Println(val, ok)  // 0 false  (channel yopiq, bo'sh)

 

For range bilan

ch := make(chan int, 5)
for i := 1; i <= 5; i++ {
   ch <- i
}
close(ch)  // yopmasa for range cheksiz kutadi!

for val := range ch {
   fmt.Println(val)  // 1, 2, 3, 4, 5 chiqadi keyin to'xtaydi
}

Channel'ni faqat yuboruvchi yopishi kerak! Qabul qiluvchi yopsa — yuboruvchi panic bo'ladi. Yopilgan channel'ga yuborish ham panic beradi.

Select operatori

select — Go'dagi channel uchun maxsus konstruktsiya. Bir vaqtda bir nechta channel'ni kuzatib, birinchi tayyor bo'lganini tanlaydi.

Asosiy select misoli

func main() {
   ch1 := make(chan string)
   ch2 := make(chan string)

   go func() { ch1 <- "ch1 dan keldi" }()
   go func() { ch2 <- "ch2 dan keldi" }()

   for i := 0; i < 2; i++ {
       select {
       case msg1 := <-ch1:
           fmt.Println(msg1)
       case msg2 := <-ch2:
           fmt.Println(msg2)
       }
   }
}

Ikkita channel bir vaqtda tayyor bo'lsa, select tasodifiy birini tanlaydi. Bu Go runtime tomonidan kafolatlangan xulq-atvordir.

Default bilan (non-blocking)

select {
case val := <-ch:
   fmt.Println("Qabul qilindi:", val)
default:
   fmt.Println("Hech narsa yo'q, davom etamiz")  // bloklanmaydi
}

Timeout bilan

select {
case val := <-ch:
   fmt.Println("Qabul qilindi:", val)
case <-time.After(3 * time.Second):
   fmt.Println("3 soniya o'tdi, timeout!")  // vaqt tugadi
}

 

 

Xatolardan saqlanish

Deadlock — o'lik qulf
Deadlock: barcha goroutinlar bir-birini kutganda dastur qotib qoladi.
 

// XATO: Deadlock!
func main() {
   ch := make(chan int)
   ch <- 1  // main goroutine qotib qoladi — qabul qiluvchi yo'q!
   // hech qachon chiqmaydi
}

// TO'G'RI:
func main() {
   ch := make(chan int)
   go func() { ch <- 1 }()  // boshqa goroutine yuboradi
   val := <-ch
   fmt.Println(val)
}

Yopilgan channel'ga yuborish

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Panic ushlab olindi:", r)
        }
    }()

    ch := make(chan int)
    close(ch)
    ch <- 1 // panic
}

Nil channel

var ch chan int  // nil channel

// Nil channel'ga yuborish/qabul — CHEKSIZ bloklanadi
// ch <- 1      // forever blocked
// val := <-ch  // forever blocked

// Lekin select'da nil case — har doim o'tkazib yuboriladi
select {
case val := <-ch:  // ch nil bo'lsa — bu case hech qachon tanlanmaydi
   fmt.Println(val)
default:
   fmt.Println("ch nil, o'tdik")
}

Goroutine oqishi (Goroutine leak)
Channel'ni yopmaslik goroutinlarning "muzlab qolishi"ga olib keladi — ular endi hech qachon tugamaydi.
 

// XATO: Yopilmagan channel — goroutine oqishi
func badExample() <-chan int {
   ch := make(chan int)
   go func() {
       for i := 0; ; i++ {
           ch <- i
       }
       // close(ch) hech qachon chaqirilmaydi!
   }()
   return ch
}

// TO'G'RI: done channel bilan to'xtatish
func goodExample(done <-chan struct{}) <-chan int {
   ch := make(chan int)
   go func() {
       defer close(ch)
       for i := 0; ; i++ {
           select {
           case ch <- i:
           case <-done:  // to'xtatish signali keldi
               return
           }
       }
   }()
   return ch
}

 

 

 

Namunaviy Kafe stsenariyasi

Tushuntirishni osonlashtirish uchun Partier, Candier va Stringerlar yuritadigan kafe stsenariyasini ko'ramiz:

  • Partier — kassada turadi, mijozlardan buyurtma qabul qiladi
  • Candier — oshxonada qahva tayyorlaydi (#1 barista)
  • Stringer — oshxonada qahva tayyorlaydi (#2 barista)

Qahva tayyorlash buyurtma qabul qilishga qaraganda ko'proq vaqt oladi. Shu sababli Partier buyurtmalarni channel orqali oshxonaga — Candier va Stringerga — uzatadi.

Kafe stsenariyasi: Partier, Candier va Stringer rollari

Unbuffered Channel

Unbuffered channel ichki bufersiz channel. Yaratish: ch := make(chan Order) yoki make(chan Order, 0).

Bu turdagi channelda jo'natish va qabul qilish bir vaqtda sodir bo'lishi kerak — biri tayyor bo'lmaguncha ikkinchisi kutib turadi.

1-holat: Bo'sh kanal — ikkalasi kutmoqda
Channelda hech qanday buyurtma yo'q. Candier ham, Stringer ham bo'sh, yangi buyurtma kelgunicha kutib turishadi.

Unbuffered channel bo'sh: Candier va Stringer kutmoqda (order := <-ch da bloklangan)

2-holat: Partier buyurtma jo'natadi
Yangi buyurtma keldi. Partier uni channelga qo'yadi va barista olgunicha kutadi — ya'ni ch <- order da bloklanadi. Candier yoki Strinfer olgunicha Partier yangi buyurtma qabul qila olmaydi.

Partier ch <- order1 da bloklangan; Candier yoki Stringer olishini kutmoqda

3-holat: Candier birinchi buyurtmani oldi
Candier va Stringer ikkalasi ham tayyor bo'lganda qaysi biri olishini Go runtime aniqlaydi — bu nondeterministik. Aytaylik, Candier birinchi buyurtmani oldi. Candier qahva tayyorlashga kirishadi, Partier esa blokdan chiqib yangi buyurtma kutishga o'tadi.

Candier order1'ni oldi; Partier blokdan chiqdi, Stringer hali bo'sh

4-holat: Ikkinchi buyurtma — yana Candier oldi
Ikkinchi buyurtma keldi. Bu safar ham Candier va Stringer ikkalasi bo'sh (Candier avvalgi buyurtmani tugatdi deb faraz qilamiz). Go runtime yana Candier ni tanladi — nondeterministik. Natija oldingi holat bilan bir xil.

order2 keldi; Candier va Stringer ikkalasi bo'sh edi — Candier yana oldi (nondeterministik)

5-holat: Uchinchi buyurtma — Stringer oldi
Uchinchi buyurtma keldi. Endi Candier order2 ni tayyorlayapti, shuning uchun faqat Stringer bo'sh. Stringer order3 ni oladi. Ikkala barista parallel ishlaydi.

order3 keldi; Candier band — faqat Stringer bo'sh, Stringer order3 ni oldi

6-holat: To'rtinchi buyurtma — Partier bloklanadi
To'rtinchi buyurtma keldi. Candier ham, Stringer ham band. Partier ch <- order4 da bloklanadi — kanal bufersiz bo'lgani uchun kimdir olmagunicha buyurtmani qo'ya olmaydi.

Muhim eslatma: Ko'pchilik make(chan Order) (bufer=0) va make(chan Order, 1) (bufer=1) ni chalkashtirib yuboradi. Bufer=0 da ch <- order4 darhol bajarilmaydi — qabul qiluvchi kutiladi. Bufer=1 da esa darhol bajariladi va Partier davom etishi mumkin. 

Candier ham Stringer ham band — Partier ch <- order4 da bloklandi

Buffered Channel

Unbuffered channel ishlaydi, lekin umumiy throughputni cheklaydi. Yechim — buffered channel: ichki navbat (bufer) mavjud bo'lgani uchun jo'natuvchi barcha uyalar to'lmaguncha bloklanmaydi.

ch := make(chan Order, 3)  // sig'im = 3

Endi Candier va Stringer band bo'lsalar ham, Partier channelga 3 tagacha buyurtma qo'yib, yangi mijozlarni qabul qilishda davom eta oladi.

Buffered channel ishlash tartibi

  • Partier order1 ni qo'yadi — bufer: [order1]. Bloklanmaydi.
  • Partier order2 ni qo'yadi — bufer: [order1, order2]. Bloklanmaydi.
  • Partier order3 ni qo'yadi — bufer: [order1, order2, order3]. Hamon bloklanmaydi.
  • Partier order4 ni qo'ymoqchi — bufer to'lgan, bloklanadi.

Bufer FIFO (First In, First Out) tartibida ishlaydi. Candier yoki Stringer bitta buyurtmani olgach, bufer bo'shaydi va Partier davom etadi.

Buffered channel kafe unumdorligini oshiradi, lekin bufer o'lchami to'g'ri tanlanishi kerak. Juda katta bufer — uzoq kutish vaqti. Ba'zan ortiqcha buyurtmani qabul qilmaslik qabul qila olib vaqtida bajara olmaslikdan yaxshiroqdir.

 

 

 

Channels implementatsiyasi chuqurroq tahlili

Channel’larni implementatsiya qilish haqida gapirganda, biz oddiy usulni — lock'lar bilan himoyalangan navbatni — tasavvur qilishimiz mumkin. Aslida, Go ham shunga o'xshash yondashuvni qo'llaydi, va bu hchan strukturasida mujassamlashgan. Ushbu struktura channel implementatsiyasining markaziy qismidir.
Go'ning channel implementatsiyasi uch asosiy strukturaga asoslanadi: hchan, waitq va sudog.
 

hchan strukturasi channel'lar ichki ishlash tamoyilining yuragida turadi. U goroutinlar o'rtasida ishonchli muloqotni ta'minlab, kanalning turli xil xatti-harakatlarini boshqarish uchun mo'ljallangan. Har bir maydonning rolini oldinga borganimizda chuqurroq ko'rib chiqamiz.

Keling, hchan strukturasi qanday ko'rinishda ekanligini ko'rib chiqaylik:

type hchan struct {
    qcount   uint           // Navbatdagi umumiy ma'lumotlar soni
    dataqsz uint           // Channel buferining o'lchami
    buf      unsafe.Pointer // Ma'lumot elementlari massiviga ko'rsatkich
    elemsize uint16         // Har bir elementning o'lchami (element turiga qarab belgilanadi)
    closed   uint32         // Channel yopilganligini bildiruvchi bayroq
    elemtype *_type         // Channel'ga yuboriladigan elementlarning turi
    sendx    uint           // Ma'lumot yuborish uchun navbatdagi uyaning indeksi
    recvx    uint           // Ma'lumot qabul qilish uchun navbatdagi uyaning indeksi
    recvq    waitq          // Kutayotgan qabul qiluvchilar navbati
    sendq    waitq          // Kutayotgan yuboruvchilar navbati
    lock     mutex          // Channelni himoya qiluvchi mutex
}

Channel'ni ishga tushirish

Channel yaratilganda, Go hchan strukturasini heapda joylashtiradi va unga ko'rsatkich qaytaradi. Demak, channel — bu hchan turidagi o'zgaruvchiga ko'rsatkichdan iborat.

Yuqorida aytilganidek, biz buferlangan channel'ni ishga tushirganimizda, channel uzunligiga teng bufer yaratiladi va elementlarni enqueue/dequeue qilish uchun kutadi. Tasavvur qilingki, biror element enqueue qilinadi — u elementni buferga qo'yadi, so'ngra dequeue jarayoni elementni buferdan oladi.

ch := make(chan int, 3)  // O'lcham 3 bo'lgan bo'sh channel yaratadi.
ch <- 3  // channel'ga element qo'shadi va sendx indeksini 1 ga oshiradi.
ch <- 4
ch <- 5  // Uchinchi element; sendx 3 ga yetdi (bufer to'ldi, circular)
v := <-ch  // Navbatdan element oladi va recvx indeksini 1 ga oshiradi.
Buferlangan channel'ning ishlash tartibi 

Xabarlar yuborish va qabul qilish jarayonni ochish

Keling, goroutinlar channel'lar orqali qanday muloqot qilishini chuqurroq o'rganaylik va channel'lar ichki mexanizmi bilan tanishamiz.

Quyidagi stsenariyni ko'rib chiqing:

Yuboruvchi (G1) va qabul qiluvchi (G2):

Bizning misolimizda ikkita asosiy goroutine mavjud: G1 (yuboruvchi) va G2 (qabul qiluvchi). G1 topshiriqlar ro'yxatiga ega va ularni channel orqali G2 ga ishlashi uchun yuboradi. Mana soddalashtirilgan kod:

// G1
func main() {
    tasks := []string{"task1", "task2", "task3"}
    
    ch := make(chan string, 3)
    
    for _, task := range tasks {
        ch <- task
    }
    go worker(ch)
}

// G2
func worker(ch chan string) {
    for {
        t := <-ch
        process(t)
    }
}

G1 channel'ga ma'lumot yuborishi:

G1 channel'ga topshiriq yuborganda (bufer bo'sh deb faraz qilganda), Go ishlash muhiti quyidagi amallarni bajaradi:

  1. Qulflash (lock)
  2. task₀ nusxasini yaratish va uni buf ga enqueue qilish
  3. Qulfni bo'shatish va G1 ning davom etishiga ruxsat berish

G2 channel'dan ma'lumot qabul qilishi:

Endi G2 kanaldan ma'lumot qabul qilish uchun rejalashtirilganda, Go ishlash muhiti yuqoridagi amallarni aksincha bajaradi:

  1. Channel bo'yicha qulflash.
  2. Buferdan task₀ ni dequeue qilish, nusxa yaratish va uni v o'zgaruvchisiga yuklash.
  3. Qulflashni bo'shatish va G2 ning davom etishiga ruxsat berish.

Xotira buferining nusxalash mexanizmi xotira xavfsizligini ta'minlaydi. Ikkala goroutine ulashgan yagona xotira — bu mutex bilan himoyalangan hchan strukturasidir. Qolgan barcha narsa, masalan topshiriqlarning o'zi, xotira nusxalash orqali uzatiladi.

 

Bloklash holati

G1 va G2 ma'lumot almashganidan so'ng, bufersiz channel yoki to'liq buferlangan channel bilan bog'liq stsenariyni o'ylab ko'raylik.

Tasavvur qiling: G2 bitta topshiriqni qayta ishlashga juda ko'p vaqt sarflayapti. Bu G2 ning asenkron channel'dan yangi topshiriqlar qabul qila olmasligiga olib keladi. Boshqa tomondan, G1 esa kanalga yangi topshiriqlar yuborishda davom etadi va bloklanmaydi.

Ammo, agar biz to'liq to'lgan buferlangan channel bilan ishlayotgan bo'lsak, G1 channel'ga yangi topshiriqlar yubora olmaydi. Natijada, G1 ning bajarilishi to'xtaydi yoki bloklanadi — va faqat qabul qilish amali sodir bo'lgach davom etadi.

Endi tushundikki, agar yuboruvchi yoki qabul qiluvchi goroutine tayyor bo'lmasa yoki navbat to'la bo'lsa, goroutinlar bloklanadi. Xo'sh, channel qabul qiluvchi va yuboruvchi goroutinlar haqida qanday xabar topadi? Mana shu yerda hchan'ning recvq va sendq maydonlari ishga kiradi.

Keling, recvq va sendq rollarini o'rganaylik. Bu ikkalasi waitq deb ataladigan muhim ma'lumotlarni o'z ichiga oladigan tur o'zgaruvchilaridir.

sendq — channel'ga ma'lumot yuborishga urinayotgan bloklangan goroutinlarni saqlaydi.

recvq — channel'dan ma'lumot o'qishga urinayotgan bloklangan goroutinlarni saqlaydi.

type waitq struct {
    first *sudog  // Navbatdagi birinchi sudog ga ko'rsatkich
    last  *sudog  // Navbatdagi oxirgi sudog ga ko'rsatkich
}
// waitq — bu navbatda kutayotgan goroutinlar haqidagi ma'lumotlarni
// o'z ichiga olgan bog'liq ro'yxat (linked list).

Sudog nima?

Bu kutish navbatlari kontekstida goroutineni ifodalovchi tuzilma. Uni navbatda kutayotgan goroutinlar haqidagi ma'lumotlarni tashuvchi deb o'ylang. U sudog deb nomlanadi.

Yuqorida aytilganidek, ikki stsenariy yuzaga kelishi mumkin: yuboruvchi qabul qiluvchini kutib bloklanadi yoki qabul qiluvchi yuboruvchini kutib bloklanadi.

Ushbu kodni ko'rib chiqing:

// G1
func main() {
    tasks := []string{"task1", "task2", "task3"}
    
    ch := make(chan string, 3)
    
    for _, task := range tasks {
        ch <- task
    }
    // channel'ga task4 yuborilmoqda
    ch <- "task4"
    go worker(ch)
}

// G2
func worker(ch chan string) {
    for {
        t := <-ch
        process(t)
    }
}

Yuboruvchi qabul qiluvchini kutib bloklanadi:

Yuqoridagi kodda biz task4 ni channel'ga yuborishga harakat qilmoqdamiz — ammo faol qabul qiluvchi yo'q. Bunday holatda, bloklangan yuboruvchi goroutine G1 sudog strukturasiga o'raladi va channel'ning sendq ga joylashtiriladi.

1*XgzhTAY3Qi9PwINXfUVI0A.png (2038×872)

Quyidagilar sodir bo'ladi:

  1. Channel o'zi uchun sudog yaratadi va uni hchan strukturasining sendq ga biriktiradi. Keyin kanal Go ishlash muhitining rejalashtiruvchisini G1 ni to'xtashiga (park) chaqiradi.
  2. Go ishlash muhitining rejalashtiruvchisi G1 ning holatini "running" dan "waiting" ga o'zgartiradi.
  3. Keyinchalik, qabul qiluvchi yangi topshiriqlar qabul qilishga tayyor bo'lganda, u sudog ni sendq'dan chiqaradi va G1 yana ishga rejalashtiriladi.

Qabul qiluvchi yuboruvchini kutib bloklanadi:

Endi qabul qiluvchi (G2) ma'lumot kutayotgan stsenariyni ko'raylik. Bu holatda G2 sudog strukturasiga o'raladi — xuddi yuboruvchining holatiga o'xshab. Ushbu sudog channel'ning recvq ga joylashtiriladi va G2 bloklangani hamda channel'ga ma'lumot yuborilishini kutayotgani ko'rsatiladi.

Qabul qiluvchi yuboruvchini kutib davom etadi

G1 (yuboruvchi) bajarilish uchun rejalashtirilganda, bloklangan qabul qiluvchi (G2) ni davom ettirish uchun ikki asosiy usul mavjud:

  • Enqueue usuli: G1 ma'lumotni kanalning buferiga enqueue qiladi, kutayotgan G2 ni recvq dan chiqaradi va rejalashtiruvchiga G2 ishga tayyor ekanligini bildiradi.
  • Optimallashtirilgan nusxalash usuli: G1 topshiriq obyektini to'g'ridan-to'g'ri sudog.elem maydonidan G2 stekiga ajratilgan xotira joyiga nusxalaydi.

Nega G1 task0 ni to'g'ridan-to'g'ri G2 stekiga nusxaladi, enqueue qilishning o'rniga?

Optimallashtirilgan nusxalash usuli odatiy bo'lmagan, ammo samarali yondashuvdir. Har bir goroutine o'z alohida stek maydoniga ega bo'lgani va goroutinlar bir-birining holatiga to'g'ridan-to'g'ri kirmasligi sababli, G1 G2 ning stek ko'rsatkichini to'g'ridan-to'g'ri boshqarishi mumkin. Bu G2 qulflashi va channel'ning buferini o'zgartirish zaruriyatini yo'q qiladi. Ushbu optimizatsiya xotira nusxalash yukini kamaytiradi va umumiy sinxronizatsiya xarajatlarini pasaytirib ishlashni yaxshilaydi.

 

Qo'shimcha o'rganish:

Manbalar:

https://go.dev/

https://gobyexample.com/channels

Go Concurrency Visually Explained — Channel

Exploring the Depths of Golang Channels

Golang: channels implementation

What are channels used for?