Tarkibga o'tish

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) — hisoblagichga n qo‘shadi;
  • Done() — hisoblagichni bittaga kamaytiradi;
  • Wait() — hisoblagich nol bo‘lguncha joriy goroutineni kutdiradi.

Oddiy oqim quyidagicha:

  1. bajariladigan ish hisoblagichga qo‘shiladi;
  2. goroutine ishga tushiriladi;
  3. goroutine tugaganda Done() chaqiradi;
  4. kutuvchi kod Wait() orqali hisoblagich nolga tushishini kutadi.

WaitGroupning zero value qiymati ishlatishga tayyor. Uni konstruktor bilan yaratish shart emas:

var wg sync.WaitGroup

Birinchi misol

Quyidagi dastur beshta goroutine ishga tushiradi va ularning barchasi tugashini kutadi.

main.go
package main

import (
    "fmt"
    "sync"
)

func greet(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("Salom!", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go greet(i, &wg)
    }

    wg.Wait()
    fmt.Println("Barcha goroutine yakunlandi")
}

Natijaning mumkin bo‘lgan ko‘rinishlaridan biri:

Salom! 5
Salom! 1
Salom! 2
Salom! 3
Salom! 4
Barcha goroutine yakunlandi

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:

// Noto‘g‘ri misol:
// go func() {
//     wg.Add(1)
//     defer wg.Done()
//     work()
// }()
// wg.Wait()

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:

wg.Add(1)
go func() {
    defer wg.Done()
    work()
}()

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
package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(2)

    for i := 1; i <= 2; i++ {
        go func(id int) {
            defer wg.Done()
            fmt.Println("Ish tugadi:", id)
        }(i)
    }

    wg.Wait()
    fmt.Println("Barcha ish tugadi")
}

Natijaning mumkin bo‘lgan ko‘rinishi:

Ish tugadi: 2
Ish tugadi: 1
Barcha ish tugadi

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.

main.go
package main

import (
    "fmt"
    "sync"
)

type file struct {
    name string
    size int64
}

func main() {
    files := []file{
        {name: "users.json", size: 1200},
        {name: "orders.json", size: 3400},
        {name: "report.csv", size: 800},
    }

    sizes := make([]int64, len(files))
    var wg sync.WaitGroup

    for i, item := range files {
        wg.Add(1)
        go func(index int, current file) {
            defer wg.Done()
            sizes[index] = current.size
        }(i, item)
    }

    wg.Wait()

    var total int64
    for i, item := range files {
        fmt.Printf("%s: %d bayt\n", item.name, sizes[i])
        total += sizes[i]
    }
    fmt.Println("Jami:", total, "bayt")
}

Natija:

users.json: 1200 bayt
orders.json: 3400 bayt
report.csv: 800 bayt
Jami: 5400 bayt

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:

func work(wg *sync.WaitGroup) {
    defer wg.Done()
}

go vet ayrim WaitGroup nusxalash holatlarini aniqlay oladi:

go vet ./...

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?

  • WaitGroup hisoblagichi Add bilan oshadi, Done bilan kamayadi va nol bo‘lganda Wait qaytadi;
  • Add(1) goroutine ishga tushirilishidan oldin chaqiriladi;
  • Done() odatda defer bilan ishlatiladi;
  • hisoblagich manfiy bo‘lsa panic, nolga tushmasa deadlock ehtimoli bor;
  • WaitGroup ishlatila boshlagach nusxalanmaydi;
  • u natija, xato, cancellation yoki concurrency limitini boshqarmaydi;
  • Donedan oldingi yozuvlar tegishli Wait qaytgach 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.