Go’da channel bilan ishlash
Channel — goroutinelar orasida ma’lum turdagi qiymatlarni uzatish vositasi. U faqat ma’lumot yuborish uchun emas, goroutinelarning ishini bir-biri bilan sinxronlashtirish uchun ham ishlatiladi.
Oddiy qilib aytganda, bir goroutine channelga qiymat yuboradi, boshqa goroutine esa shu qiymatni qabul qiladi.
Masalan, bir nechta goroutine turli tashqi APIlardan ma’lumot olayotgan bo‘lsin. Har bir goroutine o‘z natijasini umumiy mapga yozishi mumkin. Lekin bir nechta goroutine bitta mapga bir vaqtda yozsa, alohida sinxronizatsiya kerak bo‘ladi.
Buning o‘rniga har bir goroutine o‘z natijasini channel orqali bitta yig‘uvchi goroutinega yuborishi mumkin. Shunda umumiy ma’lumotga bir nechta goroutine bir vaqtda yozishi zarurati kamayadi.
Bu yerda muhim bir nozik joy bor. Channel orqali pointer, slice, map yoki boshqa reference xususiyatiga ega qiymatlarni ham yuborish mumkin. Channel bunday qiymatning yuborilishi va qabul qilinishini sinxronlashtiradi.
Lekin channel yuborilgan obyektning ichidagi ma’lumotni avtomatik himoya qilmaydi.
Masalan, channel orqali bir obyektga pointer yuborildi. Keyin yuboruvchi goroutine ham, qabul qiluvchi goroutine ham o‘sha obyektni bir vaqtda o‘zgartirsa, data race yuz berishi mumkin.
Demak:
- channel qiymat uzatishni sinxronlashtiradi;
- lekin yuborilgan obyektning keyingi concurrent mutation holatini avtomatik himoya qilmaydi.
Channel yaratish
Channel turi chan kalit so‘zi va channel orqali uzatiladigan element turi bilan yoziladi:
Bu yerda ch o‘zgaruvchisining turi chan int.
Bu shuni anglatadiki, channel orqali int qiymatlar yuboriladi va qabul qilinadi.
Lekin yuqoridagi kod hali ishlaydigan channel yaratmaydi. chning qiymati nil bo‘ladi.
Amalda ishlatiladigan channel odatda make yordamida yaratiladi:
Bu kod int qiymatlar bilan ishlaydigan bufersiz channel yaratadi.
chan int faqat int qiymatlarni qabul qiladi. Masalan, quyidagi kod compile time xatosiga olib keladi:
Sababi "salom" qiymati string turida, channel esa int qiymatlarni kutmoqda.
Demak, channel tur xavfsiz. Qaysi tur ko‘rsatilgan bo‘lsa, faqat shu turdagi qiymatlar yuboriladi.
Channel yaratish uchun hech qanday alohida paket import qilish kerak emas. make — Go tilining built-in funksiyasi.
Yuborish va qabul qilish
Channel bilan ishlashda <- operatori ishlatiladi.
Qiymat yuborish:
Bu yerda value qiymati ch channeliga yuboriladi.
Qiymat qabul qilish:
Bu yerda ch channelidan qiymat olinib, value o‘zgaruvchisiga yoziladi.
<- belgisi qiymat qaysi tomonga oqayotganini ko‘rsatadi.
Quyidagi kodda:
qiymat valuedan channel tomonga ketmoqda.
Quyidagi kodda esa:
qiymat channeldan value tomon kelmoqda.
Yuborish va qabul qilish operatsiyalari bloklovchi bo‘lishi mumkin.
Ayniqsa bufersiz channelda send va receive bir-birini kutadi.
Masalan, goroutine quyidagi qatorga keldi:
Agar shu paytda hech bir goroutine chdan qiymat olishga tayyor bo‘lmasa, yuboruvchi goroutine shu qatorda kutadi.
Xuddi shuningdek:
qatori bajarilganda hali hech kim qiymat yubormagan bo‘lsa, qabul qiluvchi goroutine kutadi.
Bufersiz channelda send va receive bir nuqtada uchrashganda qiymat uzatiladi. Shundan keyin ikkala goroutine ham davom etishi mumkin.
Shu sabab channel faqat ma’lumot uzatmaydi. U goroutinelar orasida sinxronizatsiya ham hosil qiladi.
Birinchi misol
Quyidagi misolda alohida goroutine sonning kvadratini hisoblaydi va natijani channel orqali main goroutinega yuboradi:
| main.go | |
|---|---|
Natija:
Kod qanday ishlashini bosqichma-bosqich ko‘ramiz.
Avval main ichida channel yaratiladi:
Bu bufersiz int channel.
Keyin square funksiyasi yangi goroutineda ishga tushiriladi:
square funksiyasining ikkinchi parametri quyidagicha yozilgan:
chan<- int — faqat qiymat yuborishga ruxsat berilgan channel turi.
Ya’ni square funksiyasi results channeliga qiymat yubora oladi:
Lekin shu parametr orqali qiymat qabul qilishga ruxsat yo‘q.
Bu yo‘nalishli channel ishlatishning foydasi shundaki, funksiya channel bilan qanday ishlashi kerakligi signature’ning o‘zida ko‘rinib turadi.
6 * 6 hisoblangach, 36 qiymati channelga yuboriladi.
Bu vaqtda main goroutine quyidagi qatorda kutmoqda:
main channelga qiymat kelmaguncha davom etmaydi.
square goroutine qiymat yuborishga kelganda send va receive bir-biri bilan uchrashadi:
Natijada value qiymati 36 bo‘ladi.
Keyin:
quyidagini chiqaradi:
Bu misolda alohida WaitGroup kerak emas.
Sababi main natijani channeldan olish uchun baribir kutmoqda. Natijaning qabul qilinishi square goroutine natijani hisoblab, channelga yuborish bosqichigacha yetib kelganini kafolatlaydi.
Demak, channelning o‘zi bu yerda kerakli sinxronizatsiyani ta’minlayapti.
Bir nechta natijani yig‘ish
Endi bir nechta goroutine bir vaqtda ishlaydigan misolni ko‘ramiz.
Quyidagi dastur uchta son uchun kvadrat hisoblaydi. Har bir hisob alohida goroutineda bajariladi.
Natijalar bitta channelga yuboriladi:
Natija:
Avval natijani ifodalovchi result struct yaratilgan:
Har bir natijada ikkita ma’lumot saqlanadi:
- qaysi son hisoblangan;
- uning kvadrati nechaga teng.
Masalan:
calculate funksiyasi bitta sonni qabul qiladi:
results chan<- result bu funksiya result qiymatlarini faqat channelga yuborishini bildiradi.
wg *sync.WaitGroup esa ish tugaganini mainga bildirish uchun ishlatiladi.
Funksiya boshida:
yozilgan.
Bu calculate funksiyasi tugaganda wg.Done() chaqirilishini kafolatlaydi.
Keyin natija hisoblanib, channelga yuboriladi:
main ichida:
uchta son bor.
Loop har biri uchun goroutine ishga tushiradi:
Har bir goroutine boshlanishidan oldin:
chaqiriladi.
Demak, WaitGroup uchta ish tugashini kutadi.
Bu yerda muhim bir savol paydo bo‘ladi: results channelini kim va qachon yopadi?
Channelni barcha ishchilar tugagandan keyin yopish kerak.
Shu sabab alohida goroutine yaratilgan:
Bu goroutine:
- barcha
calculategoroutinelari tugashini kutadi; - keyin
resultschannelini yopadi.
Asosiy main goroutine esa shu vaqtda natijalarni qabul qiladi:
range channel yopilmaguncha va undagi barcha qiymatlar olinmaguncha ishlashda davom etadi.
Masalan, natijalar channelga quyidagi tartibda kelishi mumkin:
yoki:
Goroutinelarning qaysi biri birinchi tugashiga kafolat yo‘q.
Shuning uchun natijalar mapga yig‘iladi:
Keyin chiqarishda numbers slice tartibidan foydalaniladi:
Shu sabab channelga natijalar turli tartibda kelgan bo‘lsa ham, ekrandagi natija doim quyidagi tartibda chiqadi:
Bu misolda juda muhim deadlock holati ham bor.
Agar quyidagicha yozilganida:
muammo yuz berishi mumkin edi.
Sababni bosqichma-bosqich ko‘ramiz.
results bufersiz channel:
Har bir worker:
qatorida qiymat yuborishga harakat qiladi.
Lekin bufersiz channelda send ishlashi uchun bir vaqtning o‘zida receiver ham kerak.
Agar main avval:
qilib tursa, u natijalarni qabul qilishni hali boshlamaydi.
Shunda holat quyidagicha bo‘ladi:
worker goroutinelar:
results <- ...
qabul qiluvchini kutmoqda
main:
wg.Wait()
workerlar tugashini kutmoqda
Workerlar qiymat yubora olmagani uchun Done()gacha yetib bormaydi.
main esa workerlar Done() qilishini kutmoqda.
Natijada ular bir-birini kutib qoladi.
Shu sabab wg.Wait() va close(results) alohida goroutineda bajarilgan.
Bu vaqtning o‘zida main range orqali qiymatlarni qabul qilib turadi.
Channelni yopish
Channelni quyidagicha yopish mumkin:
close(ch)ning ma’nosi:
Bu channel orqali boshqa yangi qiymat yuborilmaydi.
Channelni yopish uni xotiradan o‘chirib tashlamaydi.
Shuningdek, close qabul qiluvchi goroutinelarni "to‘xtat" degan buyruq ham emas.
U faqat channelga boshqa send bo‘lmasligini bildiradi.
Channelni odatda qiymat ishlab chiqaruvchi tomon yopadi.
Masalan:
Bu holatda producer boshqa qiymat yubormasligini biladi. Shuning uchun channelni yopish mas’uliyati odatda producer tomonda bo‘ladi.
Agar bir nechta producer bo‘lsa:
alohida muvofiqlashtiruvchi goroutine ularning barchasi tugaganini kutib, keyin channelni yopishi mumkin.
Oldingi misoldagi:
aynan shu vazifani bajargan.
Consumer odatda channelni yopmasligi kerak.
Sababi consumer boshqa producerlar hali qiymat yuboradimi yoki yo‘qmi, har doim ham bilmaydi.
Agar consumer channelni erta yopsa va producer keyin:
qilsa, dastur panic qiladi.
Yopilgan channel bilan ishlashda bir nechta muhim qoida bor.
Buferda qolgan qiymatlar olinadi
Agar buferli channel yopilsa, undagi qiymatlar yo‘qolmaydi.
Masalan:
Channel yopilgan bo‘lsa ham:
avval:
qiymatlarini beradi.
Qiymatlar tugagach zero value qaytadi
Channel yopilgan va unda boshqa qiymat qolmagan bo‘lsa, receive bloklanmaydi.
U element turining zero value qiymatini darhol qaytaradi.
Masalan, chan int uchun zero value:
chan string uchun:
chan bool uchun:
Yopiq channelga yuborish panic qiladi
Quyidagi kod noto‘g‘ri:
ch allaqachon yopilgan. Unga yana qiymat yuborish runtime panic keltiradi.
Channelni ikkinchi marta yopish panic qiladi
Quyidagi kod ham noto‘g‘ri:
Bir channel faqat bir marta yopilishi mumkin.
Ikkinchi close(ch) panic keltiradi.
nil channelni yopish panic qiladi
Masalan:
Bu yerda ch == nil.
nil channelni yopishga urinish panic keltiradi.
Channel yopilganini qanday aniqlash mumkin?
Muammo shundaki, yopilgan channel receive operatsiyasida zero value qaytaradi.
Masalan:
value == 0 chiqdi deb tasavvur qilamiz.
Bu ikkita narsadan biri bo‘lishi mumkin:
- channel orqali haqiqatan
0yuborilgan; - channel yopilgan va qiymatlar tugagan.
Shu ikki holatni ajratish uchun ikki qiymatli receive ishlatiladi:
Masalan:
Bu yerda ok channel holatini bildiradi.
Agar:
bo‘lsa, value haqiqiy channel qiymati.
Agar:
bo‘lsa, channel yopilgan va unda olinadigan boshqa qiymat yo‘q.
Bu holatda value element turining zero value qiymati bo‘ladi.
Masalan:
natijasi:
bo‘lishi mumkin.
Shuning uchun faqat:
deb tekshirish noto‘g‘ri.
Chunki 0 haqiqiy ma’lumot sifatida ham yuborilgan bo‘lishi mumkin.
range bilan qabul qilish
Channeldan qiymatlarni ketma-ket olish uchun range juda qulay:
Bu sikl channel orqali kelgan qiymatlarni bittadan qabul qiladi.
Masalan:
bo‘lsa, range:
qiymatlarini oladi.
Channel yopilib, undagi barcha qiymatlar tugagach sikl avtomatik tugaydi.
Bu aslida value, ok := <-ch yordamida yoziladigan siklning qulayroq ko‘rinishiga o‘xshaydi.
Mantiqan quyidagiga yaqin:
Bu yerda muhim bir nozik joy bor.
range ishlatilayotgan channel yopilmasa, sikl keyingi qiymatni kutishda davom etishi mumkin.
Masalan:
channelga barcha qiymatlar yuborildi, lekin close(ch) qilinmadi.
Sikl "boshqa qiymat bo‘lmaydi" degan signalni olmaydi. Shuning uchun keyingi receive’da kutib qoladi.
Lekin bundan "har bir channelni albatta yopish kerak" degan qoida kelib chiqmaydi.
Channelni faqat qabul qiluvchi oqim tugaganini bilishi kerak bo‘lganda yopish zarur.
Masalan, channel dastur davomida doimiy ishlatiladigan event oqimi bo‘lsa, uni ma’lum bir nuqtada yopish shart bo‘lmasligi mumkin.
nil channel
Channel turining zero value qiymati nil.
Masalan:
Bu yerda:
bo‘ladi.
nil channel ishlaydigan, lekin bo‘sh channel emas.
Bu ikki holatni aralashtirmaslik kerak.
Quyidagi channel haqiqiy channel:
Hozir unda qiymat bo‘lmasa ham, boshqa goroutine paydo bo‘lib send yoki receive qilsa, operatsiya davom etishi mumkin.
Lekin:
bilan yaratilgan nil channelda send va receive doimiy bloklanadi.
Masalan:
Bu send hech qachon muvaffaqiyatli tugamaydi.
Chunki nil channelning qarshi tomonida receive ishlashi mumkin bo‘lgan real channel strukturasining o‘zi yo‘q.
Xuddi shuningdek:
receive ham doimiy bloklanadi.
Noto‘g‘ri misol:
Bu xatti-harakat ba’zan ataylab ishlatiladi.
Masalan, select ichida ma’lum bir case’ni vaqtincha o‘chirish uchun channel o‘zgaruvchisini nil qilish mumkin.
Sababi nil channelga send yoki receive hech qachon tayyor holatga kelmaydi.
Bu usul select mavzusida batafsil ko‘rib chiqiladi.
Channel va xotira sinxronizatsiyasi
Channelning muhim vazifalaridan biri xotira ko‘rinishini sinxronlashtirishdir.
Oddiy ma’noda, bir goroutine senddan oldin biror ma’lumotni yozgan bo‘lsa va boshqa goroutine shu sendga mos receive’ni bajarsa, receive tugagandan keyin u oldingi yozuvlarni ko‘rishi mumkin.
Masalan:
data := 0
ch := make(chan struct{})
go func() {
data = 42
ch <- struct{}{}
}()
<-ch
fmt.Println(data)
Bu yerda worker avval:
qiladi.
Keyin:
orqali signal yuboradi.
main esa:
signalni qabul qiladi.
Channel orqali yuz bergan sinxronizatsiya sabab receive tugaganidan keyin workerning senddan oldingi yozuvlari qabul qiluvchi tomonda ko‘rinadigan bo‘ladi.
Bu yerda channel orqali 42ning o‘zi yuborilmagan. Faqat signal yuborilgan.
Shunga qaramay, send va receive goroutinelar o‘rtasida kerakli synchronization point yaratadi.
Lekin bundan bitta obyektni keyinchalik ikki goroutine bir paytda o‘zgartirishi mumkin degan xulosa chiqmaydi.
Masalan:
Slice channel orqali yuborildi.
Lekin slice butun backing arrayni chuqur nusxalab yubormaydi. Yuboruvchi va qabul qiluvchi bir xil backing arrayga murojaat qilishi mumkin.
Agar keyin ikkala goroutine bir paytda:
kabi yozuv qilsa, data race yuz berishi mumkin.
Shuning uchun amaliy qoida sifatida quyidagi yondashuv foydali:
Qiymat channel orqali yuborilgandan keyin uning egaligini qabul qiluvchiga topshiring.
Ya’ni sender yuborgandan keyin o‘sha mutable ma’lumotni qayta o‘zgartirmasin.
Agar ma’lumot bir nechta goroutine tomonidan umumiy ishlatilishi kerak bo‘lsa, concurrent mutationni mutex yoki boshqa mos sinxronizatsiya vositasi bilan himoya qilish kerak.
Deadlockka olib keladigan holat
Quyidagi dasturda bufersiz channel yaratilgan:
Kod compile bo‘ladi.
Lekin runtime vaqtida dastur davom eta olmaydi.
Runtime odatda quyidagiga o‘xshash xabar chiqaradi:
Sababni bosqichma-bosqich ko‘ramiz.
Avval channel yaratiladi:
Bu bufersiz channel.
Keyin:
bajariladi.
Bufersiz channeldagi send uchun boshqa tomonda receive kerak.
Masalan:
Lekin dasturda boshqa goroutine yo‘q.
mainning o‘zi send qatorida bloklangan.
Holat quyidagicha:
Dasturda davom eta oladigan boshqa goroutine qolmagani uchun Go runtime deadlockni aniqlaydi.
Lekin bu xabarni barcha deadlock holatlarida albatta ko‘ramiz deb o‘ylash kerak emas.
Masalan, real serverda ba’zi goroutinelar:
- network socket;
- timer;
- tashqi I/O;
- boshqa runtime event
kutayotgan bo‘lishi mumkin.
Dastur mantiqan biror joyda tiqilib qolgan bo‘lsa ham, runtime barcha goroutinelar to‘liq uxlab qolgan deb hisoblamasligi mumkin.
Shuning uchun mantiqiy deadlock har doim ham darhol:
xabariga olib kelmaydi.
Keng tarqalgan xatolar
Channelni noto‘g‘ri tomon yopishi
Channelni kim yopishi oldindan aniq bo‘lishi kerak.
Odatda channelni qiymat yuboruvchi tomon yopadi.
Agar receiver channelni o‘zi yopib yuborsa, boshqa sender hali ishlayotgan bo‘lishi mumkin.
Masalan:
Receiver channelni yopdi deb tasavvur qilamiz.
Shu vaqtda sender 3 hali:
qilishga ulgurmagan bo‘lishi mumkin.
Channel yopilganidan keyin sender qiymat yuborsa, panic yuz beradi.
Shuning uchun yopish mas’uliyatini:
- yagona sender;
- yoki barcha senderlar tugaganini biladigan coordinator
tomonida saqlash ma’qul.
Har bir channelni majburan yopish
Yangi boshlovchilar ba’zan har bir make(chan T)dan keyin qayerdadir close bo‘lishi kerak deb o‘ylashadi.
Bu to‘g‘ri emas.
Channelni xotirani bo‘shatish uchun yopish shart emas.
Garbage collector foydalanilmaydigan channelni va unga tegishli xotirani yig‘a oladi.
closening asosiy vazifasi boshqa:
Receiverga boshqa yangi qiymat kelmasligini bildirish.
Masalan, for range orqali oqimning tugashini bildirish uchun close kerak bo‘lishi mumkin.
Lekin channel faqat bir marta signal yuborish uchun ishlatilgan va undan keyin unga hech kim murojaat qilmasa, uni alohida yopish har doim ham zarur emas.
Yopiq channeldan cheksiz receive qilish
Yopiq channeldan oddiy receive qilish bloklanmaydi.
Masalan:
Bu receive’lar chan intning zero value qiymatini qaytarishda davom etadi:
Shuning uchun oqim tugaganini bilish kerak bo‘lsa:
ishlatiladi.
Yoki:
ishlatish mumkin.
range channel yopilib, barcha qiymatlar tugagach avtomatik yakunlanadi.
Channel data raceni butunlay yo‘q qiladi deb o‘ylash
Channel ishlatilgani bilan dastur avtomatik ravishda data racelardan holi bo‘lib qolmaydi.
Masalan:
Pointer channel orqali yuborildi:
Bu pointerning o‘zi xavfsiz tarzda bir goroutinedan boshqasiga uzatilishi mumkin.
Lekin keyin sender va receiver bir paytda:
qilsa, ular bir xil User obyektini o‘zgartiryapti.
Channel bu keyingi yozuvlarni avtomatik himoya qilmaydi.
Xuddi shu holat slice va map uchun ham muhim.
Masalan, slice yuborilganda slice header nusxalanishi mumkin, lekin uning backing arrayi umumiy bo‘lib qoladi.
Map esa o‘z ichki ma’lumot strukturasiga reference orqali ishlaydi.
Shuning uchun channel bilan ishlaganda ownership haqida o‘ylash foydali:
sender -- qiymat yuboradi --> receiver
sender -- endi o‘zgartirmaydi
receiver -- qiymatning egasi bo‘ladi
Agar ownership topshirib bo‘lmasa va ma’lumot umumiy bo‘lib qolsa, concurrent mutationni alohida sinxronlashtirish kerak.
Interviewda nimalarga e’tibor beriladi?
Channel haqida interview savollarida faqat sintaksisni bilish yetarli emas. Uning bloklanish va sinxronizatsiya xatti-harakatlarini ham tushunish muhim.
Quyidagi jihatlarga e’tibor bering:
- bufersiz channelda send va receive bir-birini kutishi mumkin;
- channel qiymat uzatish bilan birga goroutinelar orasida sinxronizatsiya ham hosil qiladi;
ch <- valueqiymat yuboradi;<-chqiymat qabul qiladi;chan<- Tfaqat yuborishga ruxsat berilgan channel turini bildiradi;- yopiq channelga send qilish panic keltiradi;
- yopiq channeldan receive avval qolgan qiymatlarni beradi;
- qiymatlar tugagach receive element turining zero value qiymatini qaytaradi;
value, ok := <-chorqali haqiqiy qiymatni channel yopilishidan ajratish mumkin;ok == falsechannel yopilganini va unda boshqa qiymat qolmaganini bildiradi;- channelni odatda sender yoki barcha senderlar tugaganini biladigan coordinator yopadi;
nilchannelga send ham, receive ham doimiy bloklanadi;nilchannel bilanmakeqilingan, lekin hozircha bo‘sh channel bir xil emas;- channel orqali pointer, slice yoki map yuborish ichki ma’lumotni avtomatik himoya qilmaydi;
- yuborilgan mutable obyekt keyinchalik bir nechta goroutine tomonidan parallel o‘zgartirilsa,
data raceyuz berishi mumkin; - channel bilan ishlaganda kim yuborishi, kim qabul qilishi, kim yopishi va qabul qiluvchi qachon to‘xtashi oldindan aniq bo‘lishi kerak.
Channelning keyingi muhim qismi bufersiz va buferli channel orasidagi farqdir. Shuningdek, channelni faqat yuborish yoki faqat qabul qilish uchun cheklash ham mumkin. Bu xususiyatlar goroutinelar o‘rtasidagi traffic va synchronization qanday ishlashiga bevosita ta’sir qiladi.