sync.WaitGroup bilan goroutinelarni kutish
sync.WaitGroup — bir guruh goroutine ishini tugatishini kutish uchun ishlatiladigan sinxronizatsiya vositasi.
Uning vazifasi oddiy: nechta ish hali tugamaganini hisoblab boradi. Bu son nolga tushganda barcha ro‘yxatga olingan ishlar tugagan deb hisoblanadi va kutayotgan goroutine davom etadi.
Oldingi mavzularda muhim bir holatni ko‘rdik: main() funksiyasi tugasa, dastur ham tugaydi. Go boshqa goroutinelarning tugashini avtomatik kutmaydi.
Masalan, goroutine ishga tushirib, keyin main() darhol qaytsa, yordamchi goroutine o‘z ishini tugatishga ulgurmasligi mumkin.
Ba’zan buning oldini olish uchun time.Sleep ishlatiladi:
Lekin bu ishonchli yechim emas.
Agar ish bir soniyadan uzoq davom etsa, dastur baribir erta tugashi mumkin. Agar ish 10 millisekundda tugasa, dastur qolgan vaqtni bekorga kutadi.
WaitGroup esa vaqtni taxmin qilmaydi. U ishning haqiqatan tugaganini kuzatadi.
WaitGroup qanday ishlaydi?
WaitGroup bilan ishlashning klassik usulida asosan uchta metod ishlatiladi:
Add(n)— tugallanmagan ishlar hisoblagichiganqo‘shadi;Done()— hisoblagichni bittaga kamaytiradi;Wait()— hisoblagich nol bo‘lguncha joriy goroutineni kutdiradi.
Oddiy ish jarayoni quyidagicha:
- Avval bajariladigan ish hisoblagichga qo‘shiladi.
- Keyin goroutine ishga tushiriladi.
- Goroutine o‘z ishini tugatganda
Done()chaqiradi. - Kutuvchi kod
Wait()orqali barcha ishlar tugashini kutadi. - Hisoblagich nolga tushganda
Wait()qaytadi.
Masalan, uchta goroutine ishlashi kerak bo‘lsa, mantiqan hisoblagich quyidagicha o‘zgaradi:
Boshlanish:
counter = 0
Add(3):
counter = 3
1-goroutine Done():
counter = 2
2-goroutine Done():
counter = 1
3-goroutine Done():
counter = 0
Wait() davom etadi
WaitGroupning zero value qiymati ishlatishga tayyor.
Shuning uchun alohida konstruktor chaqirish kerak emas:
Bu yozuvdan keyin wg.Add(), wg.Done() va wg.Wait() metodlarini darhol ishlatish mumkin.
Go 1.25 va yangi versiyalarda WaitGroup.Go()
Go 1.25dan boshlab WaitGroupda Go() metodi ham mavjud. U hisoblagichni oshiradi, yangi goroutine ishga tushiradi va funksiya tugagach hisoblagichni avtomatik kamaytiradi.
| main.go | |
|---|---|
Bu usulda wg.Add(1), go va wg.Done()ni alohida yozish shart emas. Go()ga berilgan funksiya ichida yana Done() chaqirmang, aks holda hisoblagich ortiqcha kamayadi. Bu funksiya panic qilmasligi kerak.
Add() va Done() metodlarini bilish baribir muhim. Ular Go 1.24 va undan eski versiyalardagi kodlarda hamda ish allaqachon boshqa joyda boshlangan holatlarda uchraydi. Keyingi misollarda klassik usulni ham ko‘rib chiqamiz.
Birinchi misol
Quyidagi dastur beshta goroutine ishga tushiradi. main() esa ularning barchasi tugashini kutadi.
| main.go | |
|---|---|
Natijaning mumkin bo‘lgan ko‘rinishlaridan biri:
Endi kodni bosqichma-bosqich ko‘ramiz.
Avval WaitGroup yaratiladi:
Hisoblagich dastlab 0.
Siklning har bir aylanishida:
chaqiriladi.
Bu yangi tugallanmagan ish borligini bildiradi.
Birinchi aylanishdan keyin:
Ikkinchi aylanishdan keyin:
Beshinchi aylanishdan keyin esa:
Shundan keyin goroutine ishga tushiriladi:
Bu yerda &wg ishlatilganiga e’tibor bering. Funksiyaga WaitGroupning nusxasi emas, uning pointeri uzatilmoqda.
Shuning uchun barcha goroutinelar aynan bitta WaitGroup bilan ishlaydi.
greet() ichida:
yozilgan.
Done() hisoblagichni bittaga kamaytiradi.
Masalan:
defer sabab Done() funksiya tugashidan oldin bajariladi.
main() ichidagi:
esa hisoblagich nolga tushmaguncha kutadi.
Faqat barcha beshta goroutine Done() chaqirgandan keyin quyidagi qator bajariladi:
Salomlashuv satrlarining tartibi kafolatlanmaydi.
Masalan, quyidagicha ham chiqishi mumkin:
Buning sababi goroutinelarni Go scheduler rejalashtiradi. Qaysi goroutine birinchi CPU vaqtini olishi oldindan kafolatlanmaydi.
Ammo bitta narsa kafolatlangan: "Barcha goroutine yakunlandi" satri barcha greet() chaqiruvlari tugagandan keyin chiqadi.
Nega Done() odatda defer bilan yoziladi?
Done() amalda hisoblagichni bittaga kamaytiradi.
Quyidagi ikki operatsiya mazmunan bir xil:
va:
Lekin amaliy kodda Done() ishlatish ancha tushunarli.
Uni funksiya oxirida oddiy chaqirish ham mumkin:
Bu kod ishlaydi.
Muammo funksiya bir nechta joydan qaytishi mumkin bo‘lganda boshlanadi.
Masalan:
Bu yerda value < 0 bo‘lsa, funksiya wg.Done()ga yetib bormaydi.
Natijada WaitGroup hisoblagichi kamaymay qoladi.
Shuning uchun odatda quyidagicha yoziladi:
func work(wg *sync.WaitGroup) {
defer wg.Done()
// Funksiya qaysi return yo‘lidan chiqmasin,
// Done funksiya qaytishidan oldin chaqiriladi.
}
defer chaqiruvni funksiya tugashigacha kechiktiradi.
Shuning uchun funksiya oddiy return bilan qayerdan chiqishidan qat’i nazar, Done() bajariladi.
Bu ayniqsa bir nechta return mavjud funksiyalarda foydali.
Lekin bu yerda bir nozik holat bor.
Agar funksiya umuman qaytmasa, masalan abadiy bloklanib qolsa, deferred Done() ham bajarilmaydi.
Misol:
select {} abadiy bloklanadi. Funksiya tugamaydi. Demak, Done() ham chaqirilmaydi.
Yana bir holat — panic.
Agar funksiya ichida panic yuz bersa, stack unwind jarayonida deferred funksiyalar bajariladi. Demak, wg.Done() chaqirilishi mumkin.
Ammo panic recover qilinmasa, undan keyin dastur baribir to‘xtaydi.
Addni goroutine boshlanishidan oldin chaqiring
WaitGroup bilan ishlaganda eng muhim qoidalardan biri:
Musbat
Addchaqiruvini goroutine ishga tushirilishidan oldin bajarish kerak.
Quyidagi yozuv noto‘g‘ri:
Birinchi qarashda bu kod mantiqli ko‘rinishi mumkin.
Goroutine ishga tushadi, o‘zini hisoblagichga qo‘shadi, ishni bajaradi va Done() chaqiradi.
Lekin goroutinelar qachon bajarilishini scheduler hal qiladi.
Quyidagi ketma-ketlik yuz berishi mumkin:
1. main yangi goroutineni yaratadi.
2. Yangi goroutine hali CPU olmaydi.
3. main wg.Wait()ni chaqiradi.
4. WaitGroup hisoblagichi hali 0.
5. Wait() darhol qaytadi.
6. main davom etadi yoki dastur tugaydi.
7. Yangi goroutine keyinroq Add(1)ni bajarishi mumkin.
Demak, Wait() yangi ish hisoblagichga qo‘shilmasidan oldin qaytib ketishi mumkin.
To‘g‘ri yozuv:
Bu yerda avval:
bajariladi.
Shundan keyingina goroutine yaratiladi.
Endi scheduler qanday tartibda ishlashidan qat’i nazar, Wait() kamida bitta tugallanmagan ish borligini ko‘radi.
Masalan:
counter = 0
wg.Add(1)
counter = 1
goroutine ishga tushiriladi
wg.Wait()
counter hali 1 bo‘lsa, Wait kutadi
goroutine Done()
counter = 0
Wait qaytadi
Musbat Add chaqiruvi hisoblagich nol holatda turganida, tegishli Wait()dan oldin bajarilishi kerak.
Hisoblagich mos kelmasa nima bo‘ladi?
WaitGroupning ishlashi hisoblagichning to‘g‘ri boshqarilishiga bog‘liq.
Har bir ro‘yxatga olingan ish oxir-oqibat hisoblagichdan ayrilishi kerak.
Masalan, ikkita goroutine ishlatilsa, hisoblagichni birdan 2ga oshirish mumkin:
| main.go | |
|---|---|
Natijaning mumkin bo‘lgan ko‘rinishi:
Bu yerda:
ikkita tugallanmagan ish borligini bildiradi.
Boshlang‘ich holat:
Add(2)dan keyin:
Birinchi goroutine tugaganda:
Ikkinchi goroutine tugaganda:
Shundan keyin Wait() qaytadi.
Birinchi ikki satrning tartibi o‘zgarishi mumkin. Lekin:
doimo ishchi goroutinelardan keyin chiqadi.
Hisoblagich va Done() chaqiruvlari bir-biriga mos kelmasa, ikki asosiy xato yuz beradi.
Done() yetarli chaqirilmasa
Misol:
Hisoblagich quyidagicha o‘zgaradi:
Lekin hech qachon 0ga tushmaydi.
Shuning uchun:
kutishda davom etadi.
Agar dasturda boshqa davom eta oladigan goroutine bo‘lmasa, runtime deadlock aniqlashi mumkin.
Ataylab noto‘g‘ri misol:
// Hisoblagich nolga tushmaydi:
// wg.Add(2)
// wg.Done()
// wg.Wait() // Yana bitta Done bo‘lmagani uchun kutib qoladi.
Done() ortiqcha chaqirilsa
Endi boshqa xatoni ko‘ramiz:
Hisoblagich:
WaitGroup hisoblagichi manfiy bo‘lishi mumkin emas.
Shuning uchun runtime panic chiqaradi:
Ataylab noto‘g‘ri misol:
// Hisoblagich manfiy bo‘ladi:
// wg.Add(1)
// wg.Done()
// wg.Done() // panic: sync: negative WaitGroup counter
Muhim jihat shuki, Add(n)dagi n albatta goroutinelar soni bo‘lishi shart emas.
U mantiqiy tugallanmagan ishlar sonini ifodalaydi.
Lekin odatiy kodda bitta goroutine bitta mantiqiy ish bajarsa, quyidagi juftlik tushunarli va xavfsizroq:
Bunday usulda Add va Donening bir-biriga mos kelishini ko‘rish osonroq.
Amaliy misol: fayllar hajmini parallel hisoblash
Endi WaitGroupni natija yig‘ish bilan birga ishlatadigan misolni ko‘ramiz.
Quyidagi dastur bir nechta faylga oid vazifani alohida goroutinelarga bo‘ladi.
Misol haqiqiy fayl tizimiga bog‘lanib qolmasligi uchun fayl nomlari va hajmlari oldindan berilgan.
Natija:
Bu misolda bir nechta muhim jihat bor.
Avval natijalarni saqlash uchun slice yaratiladi:
filesda uchta element bor. Demak, sizes ham uchta elementga ega bo‘ladi:
Keyin har bir fayl uchun goroutine yaratiladi:
for i, item := range files {
wg.Add(1)
go func(index int, current file) {
defer wg.Done()
sizes[index] = current.size
}(i, item)
}
Har bir goroutine sizes slicening boshqa indeksiga yozadi.
Masalan:
Shuning uchun ikki goroutine aynan bir elementga yozmayapti.
Bundan tashqari, slice uzunligi oldindan belgilangan.
Bu yerda:
ishlatilmayapti.
Bu muhim, chunki bir nechta goroutine bir xil slicega concurrent append qilsa, slice header yoki uning underlying arrayi ustida data race yuz berishi mumkin.
Anonim funksiyaga indeks va qiymat argument sifatida uzatilgan:
va parametrlar quyidagicha qabul qilinadi:
Bu har bir goroutine qaysi indeks va qaysi fayl bilan ishlayotganini aniq qiladi.
Keyin:
chaqiriladi.
main natijalarni faqat barcha goroutinelar tugagandan keyin o‘qiydi:
Shu sabab worker goroutinelarning yozuvi bilan mainning o‘qishi bir vaqtning o‘zida sodir bo‘lmaydi.
Natijalar goroutinelar qaysi tartibda tugaganiga qarab chiqarilmaydi.
Ular files slice tartibida chiqariladi:
Shu sabab yakuniy chiqish deterministik.
Haqiqiy dasturda esa current.sizeni tayyor qiymatdan olish o‘rniga goroutine masalan os.Stat chaqirishi mumkin.
Bunday real kodda qo‘shimcha masalalar paydo bo‘ladi:
- faylni tekshirishda yuz bergan xatoni saqlash;
- bir paytda juda ko‘p fayl ochilishini cheklash;
- operatsiyani cancellation orqali to‘xtatish;
- timeout qo‘llash.
WaitGroup bu vazifalarning hech birini o‘zi bajarmaydi.
WaitGroup nimani bajarmaydi?
WaitGroupning vazifasi tor va aniq:
U faqat ro‘yxatga olingan ishlarning tugashini kutadi.
U quyidagi vazifalarni bajarmaydi:
- goroutinedan natija qaytarmaydi;
- xatolarni avtomatik yig‘maydi;
- xatolarni boshqa goroutinega uzatmaydi;
- goroutinelarni bekor qilmaydi;
- timeout bermaydi;
- bir paytda ishlaydigan goroutinelar sonini cheklamaydi;
- umumiy
map, slice yoki boshqa xotirani concurrent yozishdan himoya qilmaydi.
Masalan, ikkita goroutine bir xil mapga yozsa:
var wg sync.WaitGroup
m := map[string]int{}
wg.Add(2)
go func() {
defer wg.Done()
m["a"] = 1
}()
go func() {
defer wg.Done()
m["b"] = 2
}()
wg.Wait()
bu yerda WaitGroup mavjud bo‘lsa ham, mapga concurrent yozish xavfsiz bo‘lib qolmaydi.
WaitGroup faqat ikki goroutine tugashini kutadi.
Umumiy ma’lumotni himoya qilish uchun boshqa vosita kerak bo‘ladi. Masalan:
sync.Mutex;- channel;
- goroutine ownership modeli.
Natija yoki xatoni uzatish uchun ko‘pincha channel ishlatiladi.
Bekor qilish va timeout uchun odatda:
ishlatiladi.
Bir vaqtning o‘zida ishlaydigan goroutinelar sonini cheklash uchun esa worker pool yoki semaphore kabi yondashuv qo‘llanadi.
Warning
WaitGroup ishlatilishi kodni data racedan avtomatik himoya qilmaydi. U faqat Wait() qaytishidan oldin ro‘yxatga olingan ishlar tugashini ta’minlaydi. Agar bir nechta goroutine bir xil xotiraga concurrent yozsa, alohida sinxronizatsiya kerak.
Xotira ko‘rinishi va sinxronizatsiya
WaitGroup faqat "kutish" vositasi emas. U tegishli xotira sinxronizatsiyasi kafolatlarini ham beradi.
Soddalashtirib aytganda:
Goroutine
Done()dan oldin yozgan ma’lumotlar, tegishliWait()qaytgach kutayotgan goroutine uchun ko‘rinadi.
Oldingi misolni eslaymiz:
Goroutine avval:
yozuvini bajaradi.
Keyin funksiya tugayotganida:
bajariladi.
main esa:
qaytgandan keyingina sizesni o‘qiydi.
Oqim quyidagicha:
worker:
sizes[index] = value
|
v
Done()
sinxronizatsiya
|
v
main:
Wait() qaytadi
|
v
sizes[index] o‘qiladi
Bu sababli Wait()dan keyingi o‘qish workerning oldingi yozuvini ko‘radi.
Agar main sizesni Wait()dan oldin o‘qisa, boshqa vaziyat yuz beradi.
Masalan:
go func() {
sizes[0] = 10
wg.Done()
}()
fmt.Println(sizes[0]) // juda erta o‘qilishi mumkin
wg.Wait()
Bu yerda mainning o‘qishi va workerning yozishi bir vaqtga to‘g‘ri kelishi mumkin.
Natijada data race yuz berishi ehtimoli bor.
Muhim farq:
WaitGroup workerlar bilan kutuvchi goroutine orasida kerakli sinxronizatsiyani beradi.
Lekin bu worker goroutinelarning o‘zaro concurrent yozuvlarini avtomatik xavfsiz qilmaydi.
Masalan:
ikkalasi bir vaqtda bitta qiymatga yozsa, WaitGroup buning oldini olmaydi.
Bunday holatda mutex, channel yoki boshqa mos sinxronizatsiya kerak.
WaitGroupni nusxalamang
WaitGroup ishlatila boshlagach uni nusxalamaslik kerak.
Bu qoidaning sababini tushunish muhim.
Tasavvur qiling, main ichida bitta WaitGroup bor:
Keyin uni funksiyaga qiymat sifatida yuboramiz:
Agar funksiya imzosi quyidagicha bo‘lsa:
funksiya asl WaitGroup bilan emas, uning nusxasi bilan ishlaydi.
Noto‘g‘ri misol:
// Noto‘g‘ri: WaitGroup qiymat sifatida nusxalanadi.
// func work(wg sync.WaitGroup) {
// defer wg.Done()
// }
Bu holatda taxminan quyidagi vaziyat yuz beradi:
Worker:
chaqirganda faqat nusxaning hisoblagichi o‘zgaradi:
Lekin maindagi asl obyekt:
holatida qoladi.
Natijada:
qaytmay qolishi mumkin.
To‘g‘ri variant pointer qabul qiladi:
Chaqirish:
Endi work() ham, main() ham aynan bitta WaitGroup bilan ishlaydi.
go vet ayrim WaitGroup nusxalash holatlarini topishga yordam beradi:
go vet oddiy kompilyatsiyadan o‘tadigan, lekin shubhali bo‘lishi mumkin bo‘lgan kod konstruksiyalarini statik tahlil qiladi.
Shuning uchun WaitGroup kabi nusxalanmasligi kerak bo‘lgan turlar bilan ishlaganda go vet foydali tekshiruv hisoblanadi.
WaitGroupni qayta ishlatish
Bitta WaitGroupni faqat bir marta ishlatish shart emas.
Bir guruh ishlar to‘liq tugagach, uni yangi mustaqil guruh uchun qayta ishlatish mumkin.
Masalan:
Bu yerda birinchi guruh to‘liq tugadi.
Shundan keyin:
deb yana ishlatish mumkin.
Muhim shart shuki, oldingi guruhning Wait() chaqiruvlari hali kutayotgan paytda yangi mustaqil guruh uchun musbat Addlarni boshlamaslik kerak.
Mantiqan guruhlar quyidagicha ajratilgan bo‘lishi kerak:
Quyidagidek hayot sikli esa tushunishni qiyinlashtiradi:
Amaliy kodda ko‘pincha har bir alohida operatsiya uchun lokal WaitGroup yaratish osonroq.
Masalan:
Bu yondashuv WaitGroupning qayerda yaratilgani va qayerda tugashini aniq ko‘rsatadi.
Uni global o‘zgaruvchi qilish yoki bir-biriga bog‘liq bo‘lmagan ishlar orasida uzoq vaqt saqlash kodning hayot siklini tushunishni qiyinlashtirishi mumkin.
Keng tarqalgan xatolar
Addni goroutine ichida chaqirish
Noto‘g‘ri yondashuv:
Bu yerda Wait() hisoblagich oshirilishidan oldin bajarilishi mumkin.
Natijada Wait() hisoblagichni 0 deb ko‘rib, darhol qaytadi.
To‘g‘ri yondashuv:
Asosiy qoida:
Ishni hisoblagichga avval qo‘shing, goroutineni keyin ishga tushiring.
Doneni unutish
Agar:
chaqirilgan bo‘lsa, tegishli ish oxir-oqibat hisoblagichni kamaytirishi kerak.
Aks holda:
qaytmay qoladi.
Shuning uchun worker funksiyaning boshida odatda:
yoziladi.
Bu ayniqsa bir nechta return mavjud funksiyalarda foydali.
WaitGroupni qiymat sifatida uzatish
Noto‘g‘ri:
Bu alohida nusxa yaratishi mumkin.
To‘g‘ri:
va:
Ishlatila boshlangan WaitGroupni nusxalamang.
WaitGroup natijalarni himoya qiladi deb o‘ylash
WaitGroup faqat ishlarning tugashini kutadi.
Masalan, bir nechta goroutine bitta umumiy mapga yozsa, WaitGroupning mavjudligi bu operatsiyani xavfsiz qilmaydi.
Quyidagi fikr noto‘g‘ri:
To‘g‘ri tushuncha:
Umumiy xotirani himoya qilish uchun alohida sinxronizatsiya kerak.
Add va Done sonini taxmin qilish
Hisoblagichni bajarilayotgan ishlar sonidan uzilib qolgan joyda boshqarish xatoga olib kelishi mumkin.
Masalan:
Agar keyinchalik jobs soni 10dan farq qilsa, hisoblagich va haqiqiy ishlar soni mos kelmay qoladi.
Ko‘pincha quyidagicha yozish xavfsizroq:
yoki har bir ish bilan bir joyda:
Shunda kod o‘zgarsa, hisoblagichning ishlar sonidan ajralib qolish ehtimoli kamayadi.
Interviewda nimalarga e’tibor beriladi?
sync.WaitGroup haqida savol berilganda faqat uning sintaksisini bilish yetarli emas.
Quyidagi jihatlarni tushunish muhim:
WaitGroupichki hisoblagichni boshqaradi;Add(n)hisoblagichni oshiradi;Done()hisoblagichni bittaga kamaytiradi;- hisoblagich nol bo‘lganda
Wait()qaytadi; - musbat
Add(1)goroutine ishga tushirilishidan oldin bajarilishi kerak; Done()odatdadeferbilan yoziladi;Done()yetarli bo‘lmasaWait()qaytmay qolishi mumkin;Done()ortiqcha bo‘lsa hisoblagich manfiy bo‘lib, runtime panic yuz beradi;WaitGroupishlatila boshlagach nusxalanmasligi kerak;- helper funksiyaga odatda
*sync.WaitGroupuzatiladi; WaitGroupnatijalarni qaytarmaydi;- u xatolarni boshqarmaydi;
- u cancellation bermaydi;
- u concurrency limitini belgilamaydi;
- u umumiy xotirani data racedan himoya qilmaydi;
Done()dan oldingi yozuvlar tegishliWait()qaytgach kutuvchi goroutinega ko‘rinadi;- worker goroutinelar bir xil xotiraga concurrent yozsa, alohida sinxronizatsiya baribir talab qilinadi.
Misollar
1. Bitta goroutineni kutish
Bu misol WaitGroupning eng oddiy holatini ko‘rsatadi.
Bitta yordamchi goroutine ishga tushiriladi. main() esa uning tugashini kutadi.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("Ish bajarildi")
}()
wg.Wait()
fmt.Println("main yakunlandi")
}
Avval:
orqali zero value WaitGroup yaratiladi.
Keyin:
bitta tugallanmagan ishni ro‘yxatga oladi.
Hisoblagich:
bo‘ladi.
Goroutine ichida:
yozilgan.
Goroutine tugaganda hisoblagich:
ga tushadi.
main() esa:
orqali aynan shu holatni kutadi.
Shundan keyingina:
chiqadi.
Bu misolning asosiy qoidasi: Add(1) ish boshlanishidan oldin, Done() esa ish tugaganda bajariladi.
2. Har bir ishni siklda ro‘yxatga olish
Bu misolda uchta goroutine yaratiladi.
Har bir goroutine boshlanishidan oldin tegishli ish WaitGroupga qo‘shiladi.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for id := 1; id <= 3; id++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
fmt.Println("Ishchi:", workerID)
}(id)
}
wg.Wait()
}
Sikl uch marta ishlaydi:
Har bir aylanishda:
bajariladi.
Shuning uchun uchta goroutine yaratilgach hisoblagich mantiqan 3 bo‘ladi.
Har bir goroutine bir marta Done() chaqiradi:
id qiymati anonim funksiyaga argument sifatida uzatilgan:
va:
orqali qabul qilinadi.
Bu har bir goroutine qaysi worker ID bilan ishlashini ochiq ko‘rsatadi.
Ishchi: satrlarining chiqish tartibi kafolatlanmaydi. Masalan, 3, 1, 2 tartibida chiqishi mumkin.
Bu misoldagi asosiy qoida: Add(1) goroutine ichida emas, go chaqiruvidan oldin bajariladi.
3. Ishlar sonini birdan qo‘shish
Har bir sikl aylanishida Add(1) chaqirish shart emas.
Agar ishlar soni oldindan aniq bo‘lsa, ularni birdan hisoblagichga qo‘shish mumkin.
package main
import (
"fmt"
"sync"
)
func main() {
files := []string{"a.txt", "b.txt", "c.txt"}
var wg sync.WaitGroup
wg.Add(len(files))
for _, file := range files {
go func(name string) {
defer wg.Done()
fmt.Println("Tekshirildi:", name)
}(file)
}
wg.Wait()
}
Bu yerda:
qiymati:
Shuning uchun:
aslida:
bilan bir xil natija beradi.
Hisoblagich uchta ishni kutadi.
Har bir goroutine bir marta Done() chaqirishi kerak.
Agar keyinchalik files ro‘yxatiga yangi element qo‘shilsa:
len(files) avtomatik ravishda 4 bo‘ladi.
Bu qattiq yozilgan:
dan xavfsizroq.
Bu misolning asosiy qoidasi: ishlar soni ma’lum bo‘lsa, hisoblagichni shu manbadan hisoblash mumkin.
4. WaitGroupni helper funksiyaga pointer bilan uzatish
Bu misolda worker logikasi alohida process() funksiyasiga chiqarilgan.
package main
import (
"fmt"
"sync"
)
func process(name string, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("Qayta ishlandi:", name)
}
func main() {
var wg sync.WaitGroup
items := []string{"rasm", "video"}
for _, item := range items {
wg.Add(1)
go process(item, &wg)
}
wg.Wait()
}
process() funksiyasi:
qabul qiladi.
Bu WaitGroup qiymat sifatida nusxalanmayotganini bildiradi.
Chaqirishda:
ishlatiladi.
&wg — main() ichidagi asl WaitGroupning manzili.
Shuning uchun process() ichidagi:
aynan main() kutayotgan hisoblagichni kamaytiradi.
Agar funksiya quyidagicha yozilganida:
wg nusxalanishi mumkin edi va worker asl hisoblagichni o‘zgartirmagan bo‘lardi.
Bu misolning asosiy qoidasi: ishlatila boshlangan WaitGroupni helper funksiyaga pointer orqali uzating.
5. Erta qaytishda ham Done()ni bajarish
Bu misol defer wg.Done() nima uchun foydali ekanini ko‘rsatadi.
Ba’zi qiymatlar tekshiruvdan o‘tmaydi va funksiya erta return qiladi.
package main
import (
"fmt"
"sync"
)
func validate(value int, wg *sync.WaitGroup) {
defer wg.Done()
if value < 0 {
fmt.Println("Manfiy qiymat o‘tkazib yuborildi:", value)
return
}
fmt.Println("Qabul qilindi:", value)
}
func main() {
values := []int{5, -2, 8}
var wg sync.WaitGroup
wg.Add(len(values))
for _, value := range values {
go validate(value, &wg)
}
wg.Wait()
}
Qiymatlar:
-2 uchun quyidagi shart bajariladi:
va funksiya:
bilan erta tugaydi.
Lekin Done() oldindan defer qilingan:
Shuning uchun erta return bo‘lsa ham Done() chaqiriladi.
Agar Done() faqat funksiya oxirida yozilganida:
manfiy qiymat yo‘lida u umuman bajarilmas edi.
Natijada hisoblagich nolga tushmay qolishi mumkin edi.
Bu misolning asosiy qoidasi: worker funksiyada defer wg.Done() erta return holatlarini xavfsizroq boshqaradi.
6. Natijalarni turli slice elementlariga yozish
Bu misolda bir nechta goroutine parallel hisob-kitob bajaradi.
Har biri natijani oldindan yaratilgan slicening boshqa indeksiga yozadi.
package main
import (
"fmt"
"sync"
)
func main() {
numbers := []int{2, 3, 4}
results := make([]int, len(numbers))
var wg sync.WaitGroup
wg.Add(len(numbers))
for index, number := range numbers {
go func(i, value int) {
defer wg.Done()
results[i] = value * value
}(index, number)
}
wg.Wait()
fmt.Println(results)
}
Boshlang‘ich qiymatlar:
Goroutinelar taxminan quyidagi ishlarni bajaradi:
Natijada:
hosil bo‘ladi.
Bu yerda har bir goroutine boshqa indeksga yozmoqda:
results uzunligi oldindan belgilangan:
Shuning uchun concurrent append bajarilmaydi.
main() esa:
qaytmaguncha natijalarni o‘qimaydi.
Bu workerlarning yozuvi va mainning o‘qishini bir vaqtda bajarilishidan saqlaydi.
Bu misolning asosiy qoidasi: WaitGroup natijalarni o‘zi saqlamaydi, lekin barcha workerlar tugagandan keyin natijalarni o‘qish vaqtini aniqlashga yordam beradi.
7. WaitGroupni ketma-ket bosqichlarda qayta ishlatish
Bu misolda bitta WaitGroup ikkita mustaqil bosqich uchun ketma-ket ishlatiladi.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("Birinchi bosqich")
}()
wg.Wait()
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("Ikkinchi bosqich")
}()
wg.Wait()
}
Birinchi bosqichda:
Goroutine tugaganda:
Birinchi:
qaytadi.
Shu paytda birinchi guruh to‘liq tugagan.
Keyin ikkinchi bosqich boshlanadi:
Hisoblagich yana:
bo‘ladi.
Ikkinchi goroutine tugaganda:
va ikkinchi Wait() qaytadi.
Muhim jihat: yangi guruh birinchi guruh to‘liq tugagandan keyin boshlanmoqda.
Bu misolning asosiy qoidasi: WaitGroupni qayta ishlatish mumkin, lekin oldingi guruhning kutish sikli tugagan bo‘lishi kerak.
8. Bir guruhni bir nechta joydan kutish
Bir nechta goroutine bitta WaitGroupning Wait() metodini chaqirishi mumkin.
Quyidagi misolda ikki kuzatuvchi bitta worker tugashini kutadi.
package main
import (
"fmt"
"sync"
)
func main() {
var workers sync.WaitGroup
var observers sync.WaitGroup
workers.Add(1)
go func() {
defer workers.Done()
fmt.Println("Asosiy ish tugadi")
}()
observers.Add(2)
for id := 1; id <= 2; id++ {
go func(observerID int) {
defer observers.Done()
workers.Wait()
fmt.Println("Kuzatuvchi xabardor bo‘ldi:", observerID)
}(id)
}
observers.Wait()
}
Bu yerda ikkita alohida WaitGroup bor.
Birinchisi:
asosiy worker ishini kuzatadi.
Ikkinchisi:
ikki kuzatuvchi goroutinenining tugashini kuzatadi.
Asosiy worker uchun:
bajariladi.
Ikki kuzatuvchi esa:
chaqiradi.
Hisoblagich nolga tushganda ikkala Wait() ham davom eta oladi.
Keyin har bir kuzatuvchi:
qatorini chiqaradi.
main() esa:
orqali ikkala kuzatuvchi ham tugashini kutadi.
Chiqish taxminan quyidagicha bo‘lishi mumkin:
Kuzatuvchilar tartibi kafolatlanmaydi.
Bu misolning asosiy qoidasi: bitta WaitGroupni bir nechta goroutine kutishi mumkin.
9. Har bir tashqi ish uchun ichki guruh yaratish
Ba’zan parallel ishning o‘zi ham bir nechta parallel kichik ishga bo‘linadi.
Bunday holatda tashqi va ichki WaitGrouplardan foydalanish mumkin.
package main
import (
"fmt"
"sync"
)
func section(name string, outer *sync.WaitGroup) {
defer outer.Done()
var inner sync.WaitGroup
inner.Add(2)
for part := 1; part <= 2; part++ {
go func(number int) {
defer inner.Done()
fmt.Println(name, "qism", number)
}(part)
}
inner.Wait()
}
func main() {
var outer sync.WaitGroup
outer.Add(2)
go section("A", &outer)
go section("B", &outer)
outer.Wait()
}
main() ikkita tashqi ish yaratadi:
Shuning uchun:
chaqiriladi.
Har bir section() ichida esa alohida lokal:
yaratiladi.
Muhim jihat: A va B sectionlarining inner qiymatlari bir-biridan mustaqil.
Har bir section ichida ikkita qism bor:
Keyin:
uchun alohida goroutinelar yaratiladi.
Har bir section():
orqali o‘zining ichki ikki goroutinesini kutadi.
Faqat ular tugagach section() funksiyasi qaytadi.
Funksiya qaytayotganda:
bajariladi.
Demak, outer faqat section ichidagi barcha ishlar to‘liq tugagandan keyin kamayadi.
Mantiqiy tuzilma quyidagicha:
Bu misolning asosiy qoidasi: har bir yuqori darajadagi ish o‘zining lokal WaitGroupi orqali ichki goroutinelarni kutishi mumkin.
10. Bo‘sh ishlar ro‘yxatini xavfsiz kutish
WaitGroup zero value bilan ishlaydi. Shu sabab bajariladigan ishlar bo‘lmasa ham alohida maxsus shart yozish shart emas.
package main
import (
"fmt"
"sync"
)
func main() {
jobs := []string{}
var wg sync.WaitGroup
wg.Add(len(jobs))
for _, job := range jobs {
go func(name string) {
defer wg.Done()
fmt.Println(name)
}(job)
}
wg.Wait()
fmt.Println("Bajariladigan ish yo‘q")
}
Bu yerda:
bo‘sh slice.
Demak:
qiymati:
bo‘ladi.
Shuning uchun:
aslida:
bilan teng.
Hisoblagich o‘zgarmaydi:
Sikl:
bir marta ham ishlamaydi.
Keyingi:
hisoblagich allaqachon nol bo‘lgani uchun darhol qaytadi.
Natija:
Buning uchun alohida:
tekshiruvi shart emas.
Bu misolning asosiy qoidasi: WaitGroup bo‘sh guruh bilan ham tabiiy ishlaydi. Hisoblagich nol bo‘lsa, Wait() darhol qaytadi.