sync.WaitGroup bilan goroutinelarni kutish
sync.WaitGroup — bir guruh goroutine ishini tugatguncha kutish uchun ishlatiladigan sinxronizatsiya vositasi. U bajarilayotgan ishlar sonini hisoblagichda saqlaydi. Hisoblagich nolga tushganda kutish yakunlanadi.
Oldingi darsda main() qaytsa, boshqa goroutinelar avtomatik kutilmasligini ko‘rdik. time.Sleep bilan taxminiy kutish ishonchli emas. Ish odatdagidan uzoq davom etsa, dastur erta tugaydi. Ish tez tugasa, dastur bekorga kutadi. WaitGroup vaqtni emas, ishning haqiqatan tugashini kuzatadi.
WaitGroup qanday ishlaydi?
WaitGroupning uchta asosiy metodi bor:
Add(n)— hisoblagichganqo‘shadi;Done()— hisoblagichni bittaga kamaytiradi;Wait()— hisoblagich nol bo‘lguncha joriy goroutineni kutdiradi.
Oddiy oqim quyidagicha:
- bajariladigan ish hisoblagichga qo‘shiladi;
- goroutine ishga tushiriladi;
- goroutine tugaganda
Done()chaqiradi; - kutuvchi kod
Wait()orqali hisoblagich nolga tushishini kutadi.
WaitGroupning zero value qiymati ishlatishga tayyor. Uni konstruktor bilan yaratish shart emas:
Birinchi misol
Quyidagi dastur beshta goroutine ishga tushiradi va ularning barchasi tugashini kutadi.
| main.go | |
|---|---|
Natijaning mumkin bo‘lgan ko‘rinishlaridan biri:
wg.Add(1) har bir ishni goroutine boshlanishidan oldin hisoblagichga qo‘shadi. &wg bir xil WaitGroupning manzilini funksiyaga uzatadi. defer wg.Done() funksiya qaytishidan oldin hisoblagichni kamaytiradi. wg.Wait() hisoblagich nolga tushmaguncha main goroutinesini to‘xtatib turadi.
Salomlashuv satrlarining tartibi kafolatlanmaydi. Scheduler goroutinelarni boshqa tartibda bajarishi mumkin. Faqat oxirgi satr barcha salomlashuvlardan keyin chiqishi kafolatlangan.
Nega Done() odatda defer bilan yoziladi?
Done() aslida Add(-1) bilan bir xil. Uni funksiya oxirida oddiy chaqirish mumkin, ammo funksiya bir nechta joydan return qilsa, bir yo‘lda chaqiruv unutilishi mumkin.
func work(wg *sync.WaitGroup) {
defer wg.Done()
// Funksiya qaysi return yo‘lidan chiqmasin,
// Done funksiya qaytishidan oldin chaqiriladi.
}
defer wg.Done()ni goroutine bajaradigan funksiyaning boshida yozish yakun signalini bir joyda ko‘rsatadi. Agar funksiya qaytmasdan abadiy bloklansa, deferred chaqiruv ham bajarilmaydi. Ushlanmagan panic yuz bersa, deferred funksiyalar bajariladi, ammo panic keyin butun dasturni to‘xtatadi.
Addni goroutine boshlanishidan oldin chaqiring
Quyidagi yozuv noto‘g‘ri:
Scheduler Wait()ni yangi goroutinedagi Add(1)dan oldin bajarishi mumkin. Hisoblagich hali nol bo‘lgani uchun Wait() darhol qaytadi va dastur ish tugashini kutmaydi.
To‘g‘ri tartib:
Musbat Add chaqiruvi hisoblagich nol holatda turganida Waitdan oldin bajarilishi kerak. Ish allaqachon hisoblagichga qo‘shilgach, goroutine qachon rejalashtirilishidan qat’i nazar Wait() uni hisobga oladi.
Hisoblagich mos kelmasa nima bo‘ladi?
Har bir qo‘shilgan ish yakunda hisoblagichdan ayrilishi kerak. Masalan, ikkita goroutine uchun hisoblagichni birdan oshirish mumkin:
| main.go | |
|---|---|
Natijaning mumkin bo‘lgan ko‘rinishi:
Bu yerda Add(2) ikkita ishni ro‘yxatga oladi. Har bir goroutine bir marta Done() chaqiradi. Birinchi ikki satrning tartibi o‘zgarishi mumkin, ammo yakuniy satr doim ulardan keyin chiqadi.
Hisoblagich va tugash signallari mos kelmasa, ikki xil xato yuz beradi:
Done()yetarli chaqirilmasa,Wait()qaytmaydi va dastur deadlock holatiga tushishi mumkin;Done()ortiqcha chaqirilsa, hisoblagich manfiy bo‘ladi va runtime panic chiqaradi.
Quyidagi parchalar ataylab noto‘g‘ri:
// 1. Hisoblagich nolga tushmaydi:
// wg.Add(2)
// wg.Done()
// wg.Wait() // Yana bitta Done bo‘lmagani uchun kutib qoladi.
// 2. Hisoblagich manfiy bo‘ladi:
// wg.Add(1)
// wg.Done()
// wg.Done() // panic: sync: negative WaitGroup counter
Add(n)dagi n goroutinelar soni bo‘lishi shart emas. U mantiqiy ishlar sonini ifodalaydi. Ammo amaliy kodda bitta goroutine bitta ishni bajarsa, har ish uchun Add(1) va Done() juftligi xatoni kamaytiradi.
Amaliy misol: fayllar hajmini parallel hisoblash
Quyidagi dastur bir nechta fayl haqidagi vazifani goroutinelarga bo‘ladi. Misol tashqi fayllarga bog‘lanmasligi uchun nom va hajmlar oldindan berilgan.
Natija:
Har bir goroutine sizes slicening boshqa indeksiga yozadi. Slice uzunligi oldindan belgilangan va append ishlatilmagan. Wait() qaytgandan keyingina main natijalarni o‘qiydi. Shu sabab bu misolda bir xil xotira manziliga concurrent yozish yo‘q.
Indeks va fayl qiymati anonim funksiyaga argument sifatida uzatilgan. Bu har bir goroutine qaysi qiymatlar bilan ishlashini aniq ko‘rsatadi. Natijalar goroutine tugash tartibida emas, files tartibida chiqariladi. Shu sabab chiqish deterministik.
Haqiqiy fayl dasturida goroutine os.Stat yoki fayl o‘qish amalini bajarishi mumkin. Bunda natija bilan birga xatoni ham yig‘ish, bir vaqtda ochiladigan fayllar sonini cheklash va bekor qilishni qo‘llab-quvvatlash kerak.
WaitGroup nimani bajarmaydi?
WaitGroup faqat ishlar tugashini kutadi. U quyidagi vazifalarni bajarmaydi:
- goroutinedan natija qaytarmaydi;
- xatolarni yig‘maydi yoki tarqatmaydi;
- goroutinelarni bekor qilmaydi;
- bir paytda ishlaydigan goroutinelar sonini cheklamaydi;
- umumiy
map, slice yoki hisoblagichni concurrent yozishdan himoya qilmaydi.
Natija yoki xato uchun channel yoxud mutex bilan himoyalangan tuzilma kerak. Bekor qilish va timeout uchun odatda context.Context ishlatiladi. Parallel ishlar sonini cheklash uchun worker pool yoki semaphore qo‘llanadi.
Warning
WaitGroup ishlatilishi kodni data racedan avtomatik himoya qilmaydi. U faqat Wait() qaytishidan oldin ro‘yxatga olingan ishlar tugashini ta’minlaydi. Goroutinelar bir xil xotiraga concurrent yozsa, alohida sinxronizatsiya kerak.
Xotira ko‘rinishi va sinxronizatsiya
Done() chaqiruvi undan keyin qaytadigan Wait() bilan sinxronizatsiya hosil qiladi. Sodda aytganda, goroutine Done()dan oldin bajargan yozuvlar Wait() qaytgach kutuvchi goroutinega ko‘rinadi.
Amaliy misoldagi sizes qiymatlari aynan wg.Wait()dan keyin o‘qildi. Agar main ularni Wait()dan oldin o‘qisa, goroutinelar yozuvi bilan bir paytga to‘g‘ri kelishi va data race yuz berishi mumkin edi.
Bu kafolat turli ishchi goroutinelarning bir-biriga concurrent yozishini xavfsiz qilmaydi. Ikki goroutine bir xil element yoki mapga yozsa, mutex, channel yoki boshqa mos sinxronizatsiya baribir talab qilinadi.
WaitGroupni nusxalamang
WaitGroup ishlatila boshlagach nusxalanmasligi kerak. Uning ichki holatini qiymat sifatida funksiyaga uzatish ikki alohida nusxa hosil qiladi. Goroutine nusxadagi hisoblagichni kamaytiradi, main esa asl hisoblagichni kutib qoladi.
Noto‘g‘ri funksiya imzosi:
// Noto‘g‘ri: WaitGroup qiymat sifatida nusxalanadi.
// func work(wg sync.WaitGroup) {
// defer wg.Done()
// }
To‘g‘ri variant pointer qabul qiladi:
go vet ayrim WaitGroup nusxalash holatlarini aniqlay oladi:
WaitGroupni qayta ishlatish
Bir guruh to‘liq tugab, uning barcha Wait() chaqiruvlari qaytgach, shu WaitGroupni yangi mustaqil guruh uchun qayta ishlatish mumkin. Yangi guruhning musbat Add chaqiruvlarini oldingi Wait() hali kutayotgan paytda boshlamang.
Ko‘pincha har bir mantiqiy operatsiya uchun lokal WaitGroup yaratish tushunarliroq. Uni global o‘zgaruvchi qilish yoki bir-biriga aloqasiz guruhlar orasida uzoq vaqt saqlash hayot siklini kuzatishni qiyinlashtiradi.
Keng tarqalgan xatolar
Addni goroutine ichida chaqirish
Wait() hisoblagich oshirilishidan oldin qaytishi mumkin. Add(1)ni har doim go chaqiruvidan oldin bajaring.
Doneni unutish
Hisoblagich nolga tushmaydi va kutuvchi goroutine bloklanib qoladi. Ishchi funksiya boshida defer wg.Done() yozing.
WaitGroupni qiymat sifatida uzatish
Metodlar nusxada ishlaydi va asl hisoblagich o‘zgarmaydi. Ishlatila boshlangan WaitGroupni nusxalamang; zarur bo‘lsa pointer uzating.
WaitGroup natijalarni himoya qiladi deb o‘ylash
Wait() faqat yakunni kutadi. Ishchi goroutinelar bir vaqtda umumiy mapga yozsa, panic yoki data race yuz berishi mumkin. Natijalar uchun alohida xavfsiz aloqa usulini tanlang.
Add va Done sonini taxmin qilish
Hisoblagichni bajariladigan ishlar bilan bir joyda boshqaring. Addni boshqa siklda yoki qattiq yozilgan son bilan berish kod o‘zgarganda nomuvofiqlik keltirib chiqarishi mumkin.
Interviewda nimalarga e’tibor beriladi?
WaitGrouphisoblagichiAddbilan oshadi,Donebilan kamayadi va nol bo‘lgandaWaitqaytadi;Add(1)goroutine ishga tushirilishidan oldin chaqiriladi;Done()odatdadeferbilan ishlatiladi;- hisoblagich manfiy bo‘lsa panic, nolga tushmasa deadlock ehtimoli bor;
WaitGroupishlatila boshlagach nusxalanmaydi;- u natija, xato, cancellation yoki concurrency limitini boshqarmaydi;
Donedan oldingi yozuvlar tegishliWaitqaytgach ko‘rinadi, ammo concurrent yozuvlar uchun alohida himoya kerak.
Xulosa
sync.WaitGroup bir guruh goroutine tugashini aniq kutadi. Har bir ishni Add bilan goroutine boshlanishidan oldin ro‘yxatga oling. Ishchi funksiya boshida defer wg.Done() yozing va barcha ishlar qo‘shilgach Wait() chaqiring.
Hisoblagichni muvozanatda saqlang va WaitGroupni nusxalamang. Natija, xato, bekor qilish va umumiy xotira xavfsizligi uchun alohida vositalar kerak. Keyingi darslarda goroutinelar orasida qiymat uzatish uchun channellardan foydalanishni ko‘rib chiqamiz.
Misollar
1. Bitta goroutineni kutish
Bu misolda WaitGroup bitta yordamchi goroutine 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")
}
WaitGroupning nol qiymati ishlatishga tayyor. Add(1) bitta tugallanmagan ishni ro‘yxatga oladi. Done() hisoblagichni yana nolga tushiradi va Wait() qaytadi.
2. Har bir ishni siklda ro‘yxatga olish
Bu misolda goroutine boshlanishidan oldin har bir ish uchun Add(1) chaqiriladi.
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 uchta ish yaratadi, shu sabab Add(1) uch marta bajariladi. U goroutine ichida emas, undan oldin chaqiriladi. id anonim funksiyaga argument sifatida berilib, har bir ishchi uchun alohida qiymat saqlanadi.
3. Ishlar sonini birdan qo‘shish
Bu misolda ishlar soni slice uzunligidan olinib, hisoblagich bir marta oshiriladi.
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()
}
len(files) 3ni qaytaradi va aynan uchta goroutine yaratiladi. Har bir goroutine bir marta Done() chaqirishi shart. Ishlar ro‘yxati o‘zgarsa, hisoblagich slice uzunligiga avtomatik moslashadi.
4. WaitGroupni helper funksiyaga pointer bilan uzatish
Bu misolda worker funksiya asl WaitGroupga pointer orqali murojaat qiladi.
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()
}
&wg bitta umumiy WaitGroupning manzilini uzatadi. Qiymat sifatida uzatilsa, funksiya alohida nusxani kamaytirgan bo‘lardi. Done() deferred yozilgani uchun funksiya oddiy yo‘l bilan qaytganda albatta bajariladi.
5. Erta qaytishda ham Done()ni bajarish
Bu misolda ayrim ishlar shart bajarilganda erta qaytadi.
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()
}
-2 manfiy yo‘lni tekshirish uchun tanlangan. Funksiya return bilan erta tugasa ham, deferred Done() bajariladi. Aks holda hisoblagich nolga tushmay, Wait() abadiy kutishi mumkin edi.
6. Natijalarni turli slice elementlariga yozish
Bu misolda har bir goroutine oldindan ajratilgan slicening alohida 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)
}
results uzunligi kirish slicesi bilan teng, shuning uchun barcha indekslar oldindan mavjud. Har bir goroutine boshqa elementga yozadi. main() natijalarni faqat Wait() qaytgach o‘qiydi, shu sabab yozish va o‘qish bir paytda bajarilmaydi.
7. WaitGroupni ketma-ket bosqichlarda qayta ishlatish
Bu misolda birinchi guruh to‘liq tugagach shu WaitGroup ikkinchi guruh uchun 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 Wait() qaytganda hisoblagich nol va birinchi guruh yakunlangan bo‘ladi. Shundan keyin yangi Add(1) bilan mustaqil guruh boshlash mumkin. Ikkinchi guruhni birinchi Wait() hali kutayotgan paytda qo‘shmaslik kerak.
8. Bir guruhni bir nechta joydan kutish
Bu misolda ikki kuzatuvchi bir xil ishchi guruh 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()
}
Bir nechta goroutine bir WaitGroupning Wait() metodini chaqirishi mumkin. Hisoblagich nolga tushganda ikkala kuzatuvchi davom etadi. Alohida observers guruhi main()ga kuzatuvchilar ham tugaganini kutish imkonini beradi.
9. Har bir tashqi ish uchun ichki guruh yaratish
Bu misolda tashqi goroutinelar o‘z lokal WaitGroupi bilan ichki ishlarini kutadi.
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()
}
Har bir section() chaqiruvi o‘z inner qiymatini yaratadi. Ichki hisoblagich 2, chunki har bo‘limda ikki qism bor. outer esa ikkita bo‘limning to‘liq yakunlanishini kutadi.
10. Bo‘sh ishlar ro‘yxatini xavfsiz kutish
Bu misolda ishlar slicesi bo‘sh bo‘lsa ham hisoblagich va Wait() to‘g‘ri ishlaydi.
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")
}
Bo‘sh slicening uzunligi 0, shuning uchun Add(0) hisoblagichni o‘zgartirmaydi. Sikl bir marta ham ishlamaydi va Wait() darhol qaytadi. Bu alohida if len(jobs) == 0 tekshiruvisiz ham bo‘sh guruhni boshqarishga imkon beradi.