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.