38 / 68

Goroutine sinxronizatsiyasi (WaitGroup)

Nega `time.Sleep()` yetarli emas?

Oldingi darsda (Goroutines) main() funksiyasi goroutine tugashini kutishi uchun **time.Sleep()**dan foydalangan edik:

example.go
go display("1-jarayon")
time.Sleep(time.Second * 1) // "taxminan yetarli" vaqt kutamiz

Bu usul ishlaydi, lekin ishonchsiz: agar goroutine kutilganidan sekinroq ishlasa, main() u tugamasdan yopilib qoladi; agar tezroq tugasa, dastur keraksiz vaqt behuda kutadi. Bizga aniq: "aynan shu goroutine(lar) tugaguncha kut, na ko'p, na kam" degan mexanizm kerak. Aynan shu vazifa uchun sync.WaitGroup ishlatiladi.

example.go
import "sync"

1. WaitGroup'ning uchta metodi: Add(), Done(), Wait()

example.go
package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    // funksiya tugashi bilan "men tugadim" deb belgilaydi
    defer wg.Done()

    fmt.Printf("Worker %d ishni boshladi\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        // "yana bitta goroutine kutilyapti" deb belgilaymiz
        wg.Add(1)

        go worker(i, &wg)
    }

    // barcha Add() lar Done() bilan tenglashguncha shu yerda to'xtab turadi
    wg.Wait()

    fmt.Println("Barcha workerlar tugadi")
}

Ehtimoliy natija (goroutinelar tartibi har safar farq qilishi mumkin):

kod
Worker 1 ishni boshladi
Worker 3 ishni boshladi
Worker 2 ishni boshladi
Barcha workerlar tugadi

E'tibor bering: "Barcha workerlar tugadi" qatori har doim eng oxirida chiqadi — chunki wg.Wait() aynan shuni kafolatlaydi, time.Sleep()dan farqli o'laroq.

[!IMPORTANT] wg — main() ichida e'lon qilingan, lekin worker() funksiyasiga ko'rsatkich (*sync.WaitGroup) sifatida uzatilgan. Agar uni ko'rsatkichsiz (qiymat sifatida) uzatsangiz, worker() ichida wgning nusxasi bilan ishlaysiz — asl wgga hech qanday ta'sir qilmaydi va main() abadiy Wait()da qotib qoladi!


2. Goroutine natijalarini xavfsiz yig'ish

Ko'pincha goroutinelar natijasini keyinroq ishlatish kerak bo'ladi. Buning eng oddiy yo'li — har bir goroutine o'z natijasini oldindan tayyorlangan slaysning o'ziga tegishli indeksiga yozishi:

example.go
package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    results := make([]int, 5)

    for i := 0; i < 5; i++ {
        wg.Add(1)

        // e'tibor bering: i qiymati funksiyaga ARGUMENT sifatida uzatilmoqda
        go func(idx int) {
            defer wg.Done()
            results[idx] = idx * idx
        }(i)
    }

    wg.Wait()

    fmt.Println(results)
}

Natija:

kod
[0 1 4 9 16]

Har bir goroutine faqat o'ziga tegishli results[idx] katakchasiga yozgani uchun, bu yerda bir nechta goroutine bitta umumiy o'zgaruvchini bir vaqtda o'zgartirmayapti — shuning uchun Mutex shart emas (keyingi darsda buning nima uchun muhimligini chuqur ko'ramiz).

[!NOTE] go func(idx int) { ... }(i) qolipiga e'tibor bering: i qiymati anonim funksiyaga parametr sifatida uzatilmoqda. Bu — Goning eski versiyalarida (1.22'gacha) mashhur bo'lgan "sikl o'zgaruvchisini noto'g'ri ushlab olish" xatosidan himoyalanishning eng aniq va tushunarli usuli: 2022-yilgacha bo'lgan Go versiyalarida for sikli o'zgaruvchisi barcha iteratsiyalar uchun bitta xotira katakchasini ishlatgan, shuning uchun goroutine ichida iga to'g'ridan-to'g'ri murojaat qilish kutilmagan natijalarga olib kelardi. Go 1.22 versiyasidan boshlab bu muammo tilning o'zida hal qilindi (har bir iteratsiya endi o'z nusxasiga ega), lekin idx int kabi aniq parametr orqali uzatish hamon eng tushunarli va xavfsiz uslub hisoblanadi.


3. Keng tarqalgan xato: Add()ni noto'g'ri joyda chaqirish

example.go
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        go func(id int) {
            wg.Add(1) // XATO: Add() goroutine ICHIDA chaqirilmoqda!
            defer wg.Done()
            fmt.Println("Worker", id)
        }(i)
    }

    // wg.Wait() Add() chaqirilishidan OLDIN ishga tushib ketishi mumkin
    wg.Wait()
    _ = time.Now()
    fmt.Println("Tugadi")
}

[!WARNING] wg.Add()ni har doim goroutine boshlanishidan oldin, asosiy (chaqiruvchi) goroutine'da chaqiring — hech qachon yangi goroutine'ning o'zi ichida emas! Aks holda wg.Wait() hali birorta ham Add() chaqirilmasdan turib ishga tushib, darhol "hammasi tugadi" deb noto'g'ri xulosaga kelishi mumkin — bu esa dasturning ba'zida to'g'ri, ba'zida noto'g'ri ishlashiga (poyga holatiga) sabab bo'ladi.


Xulosa: qachon qaysi metod kerak?

VazifaMetod
"Yana bitta goroutine kutilyapti" deb belgilashwg.Add(1) (goroutine boshlanishidan oldin!)
Goroutine tugaganini bildirishdefer wg.Done() (goroutine ichida, birinchi qatorda)
Barchasi tugashini kutishwg.Wait()

[!NOTE] WaitGroup faqat "hammasi tugadimi?" degan savolga javob beradi — u goroutinelar orasida ma'lumot uzatish yoki umumiy resursni himoya qilish uchun mo'ljallanmagan. Ma'lumot uzatish uchun channel, umumiy resursni himoya qilish uchun esa keyingi darsda ko'radigan sync.Mutex ishlatiladi.