73 / 73

Go Concurrency Distilled

Ushbu qo'llanma Go tilidagi parallellik (concurrency) mexanizmlari va vositalarining to'liq, jamlangan amaliy sharhidir. Har bir mavzu aniq kod namunalari, diagrammalar va amaliy tushuntirishlar bilan yoritilgan.

Mundarija


Goroutines

Go tilida parallellikning poydevori — bu goroutinelar (gorutinalar) bo'lib, ular go kalit so'zi yordamida ishga tushiriladigan funksiyalardir:

example.go
func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    go func() {
        defer wg.Done()
        fmt.Println("worker 1")
    }()
    go func() {
        defer wg.Done()
        fmt.Println("worker 2")
    }()
    wg.Wait()
}
text
worker 2
worker 1

Go runtime ushbu goroutine'larni boshqaradi va ularni protsessor yadrolarida ishlayotgan operatsion tizim oqimlari (OS threads) o'rtasida taqsimlaydi. Operatsion tizim oqimlariga nisbatan goroutine'lar nihoyatda yengil (lightweight) bo'lib, bir vaqtning o'zida yuzlab, minglab yoki hatto o'n minglab goroutine'larni bemalol yaratishingiz mumkin.

Goroutine'lar bir-biridan to'liq mustaqildir. Dasturning main funksiyasi ham alohida goroutine hisoblanadi, faqat u dastur ishga tushishi bilan avtomatik boshlanadi. Agar main funksiyasi o'z ishini tugatsa, dasturdagi barcha boshqa goroutine'lar ham darhol to'xtatiladi.

Yuqoridagi misolda biz goroutine'larning tugashini kutish uchun wait group (sync.WaitGroup) dan foydalandik. Wait group o'z ichida hisoblagichga (counter) ega. Add(n) chaqiruvi hisoblagichni n taga oshiradi, Done() esa bittaga kamaytiradi. Wait() hisoblagich nolga tenglashguncha chaqiruvchi goroutine'ni (bizning holatda mainni) bloklab turadi. Shu tariqa, main ikkala worker ham o'z ishini yakunlaguncha kutib turadi.

WaitGroup.Go metodi avtomatik tarzda wait group hisoblagichini oshiradi, funksiyani alohida goroutine'da ishga tushiradi va u tugagach hisoblagichni o'zi kamaytiradi:

example.go
func main() {
    var wg sync.WaitGroup
    wg.Go(func() {
        fmt.Println("worker 1")
    })
    wg.Go(func() {
        fmt.Println("worker 2")
    })
    wg.Wait()
}
text
worker 2
worker 1

Channels

Goroutine'lar bir-birlariga ma'lumotlarni kanallar (channels) orqali uzatishlari mumkin. Kanal xuddi bir goroutine buyumni uloqtirib, boshqa goroutine uni ilib oladigan darchaga o'xshaydi:

example.go
func main() {
    messages := make(chan string)

    go func() { messages <- "ping" }()

    msg := <-messages
    fmt.Println(msg)
}
text
ping

Kanal orqali qiymat jo'natish — sinxron operatsiyadir. Jo'natuvchi goroutine kanalga qiymat yozganda (ch <- val), u bloklanadi va kimdir ushbu qiymatni qabul qilib olguncha (<-ch) kutadi. Faqat shundan keyingina har ikkala goroutine o'z ishini davom ettiradi.

Output channel

Funksiyadan chiqish kanalini qaytarish va uni ichki goroutine orqali to'ldirish — Go tilidagi juda mashhur usullardan biridir. Bu chaqiruvchiga kanal orqali qiymatlarni qabul qilib olish imkonini beradi, kanalning egasi bo'lgan funksiya esa jarayon ustidan to'liq nazoratni saqlab qoladi:

example.go
func generate(start, stop int) chan int {
    out := make(chan int)
    go func() {
        for i := start; i < stop; i++ {
            out <- i
        }
    }()
    return out
}

Closing a channel

Barcha ma'lumotlar jo'natib bo'linganligini qabul qiluvchilarga (o'quvchilarga) bildirish uchun yozuvchi goroutine kanalni close() orqali yopadi:

example.go
func generate(start, stop int) chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for i := start; i < stop; i++ {
            out <- i
        }
    }()
    return out
}

O'quvchi kanal holatini o'qish paytida ikkinchi qiymat ("comma OK" idiomasi) orqali tekshiradi:

example.go
func main() {
    in := generate(5, 10)
    for {
        num, ok := <-in
        if !ok {
            break
        }
        fmt.Print(num, " ")
    }
}
text
5 6 7 8 9

Kanal ochiq bo'lgan vaqtda, o'quvchi navbatdagi qiymatni va true statusini oladi. Agar kanal yopilgan bo'lsa, o'quvchi ushbu turning nol qiymatini (zero value) va false statusini oladi.

Kanal faqat bir marta yopilishi mumkin. Uni qayta yopish yoki yopilgan kanalga yozish — panic keltirib chiqaradi.

Kanalni yopishning yagona sababi — barcha ma'lumotlar jo'natib bo'linganligini uning o'quvchilariga bildirishdir. Agar bu o'quvchilar uchun ahamiyatsiz bo'lsa, uni yopish shart emas. Kanal boshqa ishlatilmay qolganda, u yopiq bo'ladimi yoki yo'qmi, Go'ning axlat yig'uvchisi (Garbage Collector) uning resurslarini o'zi tozalab tashlaydi.

Channel iteration

range operatori avtomatik ravishda kanaldan navbatdagi qiymatni o'qiydi va kanal yopilganligini tekshiradi. Agar kanal yopilgan bo'lsa, u tsikldan o'zi chiqadi:

example.go
func main() {
    nums := generate(5, 10)
    for n := range nums {
        fmt.Print(n, " ")
    }
}
text
5 6 7 8 9

Slice bo'yicha range'dan farqli o'laroq, kanal ustida range juftlik emas, balki bitta yagona qiymat qaytaradi.

Directional channels

Kanal yo'nalishini belgilash orqali siz o'zingizni tasodifiy yozish yoki yopish xatolaridan himoya qilishingiz mumkin. Kanallar quyidagicha bo'lishi mumkin:

  • chan (ikki yo'nalishli): ham o'qish, ham yozish uchun (standart);
  • chan<- (send-only): faqat yozish uchun;
  • <-chan (receive-only): faqat o'qish uchun.

Siz faqat yozish uchun mo'ljallangan kanaldan o'qiy olmaysiz yoki faqat o'qish uchun mo'ljallangan kanalga yoza olmaysiz (shuningdek, uni yopa olmaysiz ham).

Odatda kanallar ham o'qish, ham yozish uchun initsializatsiya qilinadi va funksiya parametrlarida yo'naltirilgan qilib ko'rsatiladi. Go oddiy kanalni avtomatik ravishda yo'naltirilgan kanalga aylantiradi:

example.go
stream := make(chan int)

go func(in chan<- int) {
    in <- 42
}(stream)

func(out <-chan int) {
    fmt.Println(<-out)
}(stream)
text
42

Buffered channels

Buferlangan kanallar qiymatlarni saqlash uchun qat'iy o'lchamdagi buferga ega FIFO (First-In, First-Out) navbati kabi ishlaydi.

Modomiki buferda bo'sh joy bor ekan, kanalga yozish goroutine'ni bloklamaydi. Xuddi shuningdek, modomiki buferda qiymatlar mavjud bo'lsa, kanaldan o'qish goroutine'ni bloklamaydi:

example.go
stream := make(chan int, 3)
stream <- 11
stream <- 12
stream <- 13

fmt.Println(<-stream)
fmt.Println(<-stream)
text
11
12

Standart bo'yicha, agar siz bufer hajmini ko'rsatmasangiz, kanal buferlanmagan bo'ladi (bufer hajmi nolga teng).

Buferlangan kanallar o'rnatilgan len() va cap() funksiyalari bilan ishlaydi:

example.go
stream := make(chan int, 3)
stream <- 11
fmt.Println(cap(stream), len(stream))
text
3 1

Yopilgan buferlangan kanaldan o'qish buferdagi qiymatlarni va true statusini qaytarishda davom etadi. Barcha qiymatlar olingandan so'ng, u oddiy kanal kabi nol qiymat va false statusini qaytaradi:

example.go
stream := make(chan int, 1)
stream <- 11
close(stream)

val, ok := <-stream
fmt.Println(val, ok)
// 11 true

val, ok = <-stream
fmt.Println(val, ok)
// 0 false
text
11 true
0 false

nil channel

Go'dagi har qanday tur singari, kanallar ham nol qiymatga ega, bu — nil.

nil kanalga yozish yoki undan o'qish goroutine'ni cheksiz muddatga bloklaydi:

example.go
var stream chan int

go func() {
    // blocks forever
    stream <- 1
}()

// blocks forever
<-stream

nil kanalni yopish panic chaqiradi:

example.go
var stream chan int
close(stream)
// panic: close of nil channel

Select

select operatori qaysidir ma'noda switchga o'xshaydi, lekin u maxsus kanallar bilan ishlash uchun loyihalashtirilgan. Uning ishlash tartibi:

  • Qaysi caselar bloklanmaganligini tekshiradi.
  • Agar bir nechta case tayyor bo'lsa, ulardan birini tasodifiy (randomly) tanlab bajaradi.
  • Agar barcha caselar bloklangan bo'lsa va default mavjud bo'lsa, defaultni bajaradi.
  • Agar barcha caselar bloklangan bo'lsa va default mavjud bo'lmasa, caselardan biri tayyor bo'lguncha kutadi.

select konveyerlarda ma'lumotlar oqimini birlashtirish (merge) va boshqarish uchun ishlatiladi:

example.go
// merge sends values from in1 and in2 to the output channel.
func merge(in1, in2 <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for in1 != nil || in2 != nil {
            select {
            case val1, ok := <-in1:
                if ok { out <- val1 } else { in1 = nil }
            case val2, ok := <-in2:
                if ok { out <- val2 } else { in2 = nil }
            }
        }
    }()
    return out
}

// Suppose we send 10..12 to in1, 20..22 to in2,
// and call merge(in1, in2)
text
10 11 20 12 21 22

Goroutine'larni bekor qilish (cancel) uchun:

example.go
// process modifies values from in and send them to out
// until in is exhausted or cancel is closed.
func process(cancel chan struct{}, in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for val := range in {
            select {
            case out <- val*10:
            case <-cancel:
                fmt.Println("canceled")
                return
            }
        }
    }()
    return out
}

// Suppose we send values 11 and 12 to in
// and then call close(cancel)
text
110
120
canceled

Bloklanmaydigan (non-blocking) operatsiyalar uchun:

example.go
// multiplier returns a function that multiplies
// the input by 10 and sends it to the channel
// or returns an error if the channel is busy.
func multiplier(ch chan<- int) func(n int) error {
    return func(n int) error {
        select {
        case ch <- n*10:
            return nil
        default:
            return errors.New("busy")
        }
    }
}

func main() {
    nums := make(chan int, 1)
    multiply := multiplier(nums)

    err := multiply(11)
    fmt.Println(<-nums, err)
    // 110 <nil>

    err = multiply(12)
    fmt.Println(<-nums, err)
    // 120 <nil>

    err = multiply(13)
    err = multiply(14)
    fmt.Println(err)
    // busy
}
text
110 <nil>
120 <nil>
busy

Va yana ko'plab boshqa vazifalar uchun keng qo'llaniladi.


Pipelines

Konveyer (pipeline) — bu operatsiyalar ketma-ketligi bo'lib, unda har bir bosqich kirish ma'lumotlarini qabul qiladi, ularni ma'lum bir usulda qayta ishlaydi va natijani keyingi bosqichga uzatadi. Har bir operatsiyaning kirishi va chiqishi kanal hisoblanadi.

Odatdagi konveyer quyidagicha tuziladi:

  • Reader (O'quvchi): Fayl, ma'lumotlar bazasi yoki tarmoqdan kiruvchi ma'lumotlarni o'qiydi.
  • N ta protsessor (Ishlovchilar): Ma'lumotlarni o'zgartiradi, filtrlaydi, jamlaydi yoki tashqi manbalar yordamida boyitadi.
  • Writer (Yozuvchi): Qayta ishlangan ma'lumotlarni faylga, ma'lumotlar bazasiga yoki tarmoqqa yozadi.
example.go
func read[T any]() <-chan T {
    out := make(chan T)
    go func() {
        defer close(out)
        for {
            // read data from somewhere
            data := // ...
            out <- data
        }
    }()
    return out
}

func process[T any](in <-chan T) <-chan T {
    out := make(chan T)
    go func() {
        defer close(out)
        for inData := range in {
            // process the data
            outData = // ...
            out <- outData
        }
    }()
    return out
}

func write[T any](in <-chan T) <-chan struct{} {
    done := make(chan struct{})
    go func() {
        defer close(done)
        for data := range in {
            // write the data
        }
    }()
    return done
}

Output channel

Goroutine o'z ishini tugatganini boshqa goroutine'larga chiqish kanali orqali bildirishi mumkin:

example.go
func generate(start, stop int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for i := start; i < stop; i++ {
            out <- i
        }
    }()
    return out
}

func main() {
    nums := generate(5, 10)
    for n := range nums {
        fmt.Print(n, " ")
    }
}
text
5 6 7 8 9

Done channel

Agar goroutine natija qaytarishi shart bo'lmasa, u o'z vazifasini yakunlaganligini done kanali orqali bildiradi:

example.go
func work() <-chan struct{} {
    done := make(chan struct{})
    go func() {
        defer close(done)
        fmt.Println("work done")
    }()
    return done
}

func main() {
    done := work()
    <-done
}
text
work done

Cancel channel

Goroutine ishini muddatidan oldin to'xtatish uchun chaqiruvchi goroutine cancel kanalidan foydalanishi mumkin:

example.go
func generate(cancel chan struct{}, n int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for i := 1; i <= n; i++ {
            select {
            case out <- i:
            case <-cancel:
                return
            }
        }
    }()
    return out
}

func main() {
    cancel := make(chan struct{})
    defer close(cancel)

    nums := generate(cancel, 10)
    fmt.Println(<-nums)
    fmt.Println(<-nums)
    fmt.Println(<-nums)
}
text
1
2
3

Error handling

Parallel konveyerlarda xatoliklarni boshqarishning uchta asosiy yondashuvi mavjud:

➊ Birinchi xatolikda darhol to'xtash (Return on the first error):

example.go
// calculate produces answers for the given numbers.
func process(in <-chan int) (<-chan int, <-chan error) {
	out := make(chan Answer)
	errc := make(chan error, 1)
	go func() {
		defer close(out)
		for n := range in {
			ans, err := fetchAnswer(n)
			if err != nil {
				errc <- err  // return with error
				return
			}
			out <- ans
		}
		errc <- nil          // return with nil
	}()
	return out, errc
}

➋ Natija turidan (Result type) foydalanish:

example.go
// Result contains an answer or an error.
type Result struct {
	answer int
	err    error
}

// calculate produces answers for the given numbers.
func calculate(in <-chan int) <-chan Result {
	out := make(chan Result)
	go func() {
		defer close(out)
		for n := range in {
			ans, err := fetchAnswer(n)
			out <- Result{ans, err}  // return answer + error
		}
	}()
	return out
}

➌ Xatoliklarni alohida kanalga yig'ish (Collect errors separately):

example.go
// calculate produces answers for the given numbers.
func calculate(in <-chan int, errc chan<- error) <-chan int {
	out := make(chan Answer)
	go func() {
		defer close(out)
		for n := range in {
			ans, err := fetchAnswer(n)
			if err == nil {
				out <- ans   // send answer
			} else {
				errc <- err  // or error
			}
		}
	}()
	return out
}

Time

Sana va vaqt bilan ishlashdan tashqari, time paketi parallel dasturlarda vaqtga bog'liq bo'lgan operatsiyalarni boshqarish uchun juda qulay vositalarni taqdim etadi.

After

time.After() dastlab bo'sh bo'lgan kanalni qaytaradi va belgilangan timeout muddati o'tgach, ushbu kanalga joriy vaqt qiymatini yuboradi. Bu operatsiyalar uchun timeout belgilashda nihoyatda qo'l keladi:

example.go
// withTimeout executes a function with a given timeout.
func withTimeout(timeout time.Duration, fn func()) error {
    done := make(chan struct{})
    go func() {
        defer close(done)
        fn()
    }()

    // blocks until fn completes or the timer expires,
    // whichever happens first
    select {
    case <-done:
        return nil
    case <-time.After(timeout):
        return errors.New("timeout")
    }
}

withTimeout() funksiyasi fn() bajarilishini kutadi, ammo time.After() tufayli u berilgan timeout muddatidan ko'proq kutib qolmaydi:

example.go
func main() {
    var err error

    // completes in time
    err = withTimeout(
        50*time.Millisecond,
        func() { fmt.Println("work done") },
    )
    fmt.Println("err =", err)

    // gets canceled on timeout
    err = withTimeout(
        50*time.Millisecond,
        func() {
            time.Sleep(100 * time.Millisecond)
            fmt.Println("work done")
        },
    )
    fmt.Println("err =", err)
}
text
work done
err = <nil>
err = timeout

Timer

Taymer (time.Timer) — bu C kanaliga ega tuzilma bo'lib, u ishga tushganda (muddati tugaganda) ushbu kanalga joriy vaqtni yuboradi. Taymerlar kelajakdagi bajarilishlarni rejalashtirish uchun xizmat qiladi:

example.go
done := make(chan struct{})

timer := time.NewTimer(50 * time.Millisecond)
go func() {
    eventTime := <-timer.C  // blocks for 50ms
    fmt.Println("work done at", eventTime)
    close(done)
}()

<-done
text
work done at 2009-11-10 23:00:00.05

Stop() metodi taymerni to'xtatadi va agar u hali ishga tushmagan bo'lsa true, aks holda false qaytaradi:

example.go
// timer expires after 50ms
timer := time.NewTimer(50 * time.Millisecond)
go func() {
    eventTime := <-timer.C
    fmt.Println("work done at", eventTime)
}()

// after 10ms, the timer hasn't expired yet
time.Sleep(10 * time.Millisecond)

if timer.Stop() {
    fmt.Println("execution canceled")
} else {
    fmt.Println("too late to cancel")
}
text
execution canceled

Ko'pincha time.AfterFunc() o'ram funksiyasidan foydalanish ancha qulayroqdir. U d muddat kutadi va so'ng f funksiyasini yangi goroutine'da ishga tushiradi:

example.go
done := make(chan struct{})
work := func() {
    fmt.Println("work done")
    close(done)
}

// executes work after 50ms
time.AfterFunc(50*time.Millisecond, work)
<-done
text
work done

time.AfterFunc() taymer obyektini qaytaradi, shuning uchun uni ish boshlanishidan oldin bekor qilish imkoni mavjud:

example.go
// executes the function after 50ms
timer := time.AfterFunc(50*time.Millisecond, func() {})

// after 10ms, the timer hasn't expired yet
time.Sleep(10 * time.Millisecond)

if timer.Stop() {
    fmt.Println("execution canceled")
}
text
execution canceled

Agar taymer tsikl ichida ishlatilsa, har bir iteratsiyada yangi taymer yaratish o'rniga yagona taymer yaratib, uni Reset qilish xotira samaradorligi jihatidan ancha afzaldir:

example.go
// consumer reads tokens from the input channel and alerts
// if a value does not appear in a channel after an hour.
func consumer(in <-chan token) {
    const timeout = time.Hour
    timer := time.NewTimer(timeout)
    for {
        timer.Reset(timeout)
        select {
        case <-in:
            // do stuff
        case <-timer.C:
            // log warning
        }
    }
}

// Suppose we send 10,000 values to the in channel
// and measure memory usage.
text
Memory used: 4 KB, # allocations: 6

Ticker

Tikker taymerga o'xshaydi, lekin u siz uni to'xtatguningizcha doimiy ravishda belgilangan vaqt oralig'ida signallar yuborib turadi. Tikkerlar davriy vazifalarni bajarish uchun mo'ljallangan:

example.go
// fires every 50ms
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop()

go func() {
    for {
        // waits for ticker to fire on each iteration
        at := <-ticker.C
        fmt.Println("work done at", at)
    }
}()

// enough time for the ticker to fire 3 times
time.Sleep(160*time.Millisecond)
ticker.Stop()
text
work done at 2009-11-10 23:00:00.05
work done at 2009-11-10 23:00:00.10
work done at 2009-11-10 23:00:00.15

NewTicker(d) har d intervalda C kanaliga joriy vaqtni jo'natuvchi tikker yaratadi. Resurslarni bo'shatish uchun siz oxir-oqibat Stop() orqali tikkerni to'xtatishingiz shart.

Agar kanal o'quvchisi tikker tezligiga yetib kela olmasa, tikker o'tkazib yuborilgan signallarni tashlab yuboradi (ticks are skipped).


Context

Kontekstning (context.Context) asosiy maqsadi — operatsiyalarni qo'lda yoki timeout/deadline (muddat) orqali bekor qilishdir.

Funksiya kontekstni qabul qiladi va bekor qilishni tinglash uchun uning Done() kanalidan foydalanadi:

example.go
// work performs a task for 50 ms unless canceled.
// Returns an error when canceled.
func work(ctx context.Context) error {
    done := make(chan struct{})

    go func() {
        time.Sleep(50 * time.Millisecond)
        fmt.Println("work done")
        close(done)
    }()

    select {
    case <-done:
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

Qo'lda bekor qilish (context.Canceled xatosi):

example.go
func main() {
    // empty context
    ctx := context.Background()
    // manual cancellation context
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    done := make(chan struct{})
    go func() {
        // takes 50 ms unless canceled
        err := work(ctx)
        fmt.Println("err =", err)
        close(done)
    }()

    // cancels after 10 ms
    time.Sleep(10 * time.Millisecond)
    cancel()
    <-done
}
text
err = context canceled

Timeout orqali bekor qilish (context.DeadlineExceeded xatosi):

example.go
func main() {
    ctx := context.Background()
    // cancels after 10 ms
    ctx, cancel := context.WithTimeout(ctx, 10*time.Millisecond)
    defer cancel()

    done := make(chan struct{})
    go func() {
        // takes 50 ms unless canceled
        err := work(ctx)
        fmt.Println("err =", err)
        close(done)
    }()

    <-done
}
text
err = context deadline exceeded

Deadline (aniq vaqt) orqali bekor qilish (context.DeadlineExceeded xatosi):

example.go
func main() {
    ctx := context.Background()
    // cancels at now + 10 ms
    deadline := time.Now().Add(10 * time.Millisecond)
    ctx, cancel := context.WithDeadline(ctx, deadline)
    defer cancel()

    done := make(chan struct{})
    go func() {
        // takes 50 ms unless canceled
        err := work(ctx)
        fmt.Println("err =", err)
        close(done)
    }()

    <-done
}
text
err = context deadline exceeded

Kontekst qatlamli (layered) ierarxiyaga ega. Kontekst obyekti o'zgarmasdir (immutable). Mavjud kontekstga yangi xususiyatlar qo'shish uchun eski (ota) kontekst asosida yangi (farzand) kontekst yaratiladi. Ota va farzand kontekstlar o'rtasida har doim qisqaroq timeout ustun keladi. Farzand kontekst ota kontekstning timeout muddatini faqat qisqartirishi mumkin, uni uzaytira olmaydi:

example.go
func main() {
    // parent context with a 100 ms timeout
    const dur100ms = 100 * time.Millisecond
    parentCtx, cancel := context.WithTimeout(context.Background(), dur100ms)
    defer cancel()

    // child context with a 10 ms timeout
    const dur10ms = 10 * time.Millisecond
    childCtx, cancel := context.WithTimeout(parentCtx, dur10ms)
    defer cancel()

    // now the work gets canceled
    err := work(childCtx)
    fmt.Println("err =", err)
}
text
err = context deadline exceeded

Bir nechta bekor qilish chaqiruvlari to'liq xavfsizdir. Siz kontekst ustida cancel()ni xohlagancha marta chaqirishingiz mumkin. Faqat birinchi bekor qilish amalga oshadi, qolgan barcha chaqiruvlar esa shunchaki e'tiborga olinmaydi.

Siz context.WithCancelCause(), context.WithTimeoutCause() va context.WithDeadlineCause() yordamida maxsus bekor qilish sababini belgilashingiz mumkin. Bu sababni context.Cause() orqali olish mumkin:

example.go
ctx, cancel := context.WithCancelCause(context.Background())
cancel(errors.New("the night is dark"))
fmt.Println(context.Cause(ctx))
text
the night is dark

Kontekst bekor qilinganda bajariladigan maxsus tozalash funksiyasini context.AfterFunc() yordamida ro'yxatdan o'tkazishingiz mumkin:

example.go
ctx, cancel := context.WithCancel(context.Background())
cleanup := func() { fmt.Println("cleanup") }
context.AfterFunc(ctx, cleanup)
cancel()
time.Sleep(10 * time.Millisecond)
text
cleanup

Kontekst context.WithValue() orqali chaqiruv haqida qo'shimcha ma'lumotlarni ham uzata oladi, bu esa ma'lum bir kalit uchun qiymat saqlaydigan kontekst yaratadi. Biroq, kontekst orqali biznes parametrlarini uzatishdan iloji boricha qochish tavsiya etiladi. Buning o'rniga aniq funksiya parametrlari yoki maxsus struct'lardan foydalanish ancha to'g'ri va xavfsizdir.


Wait groups

sync.WaitGroup turi bir yoki bir nechta goroutine'larning ishini yakunlashini kutish imkonini beradi:

example.go
const n = 10
var wg sync.WaitGroup
wg.Add(n)
for range n {
    go func() {
        defer wg.Done()
        fmt.Print(".")
    }()
}
wg.Wait()
text
..........

WaitGroup o'zi boshqarayotgan goroutine'lar haqida hech narsa bilmaydi. U faqat ichki hisoblagich bilan ishlaydi. wg.Add(1) chaqiruvi hisoblagichni 1 taga oshiradi, wg.Done() esa 1 taga kamaytiradi. wg.Wait() hisoblagich nolga tenglashguncha chaqiruvchi goroutine'ni bloklaydi.

Go metodi Add, goroutine'ni boshlash va Done amallarini bittada birlashtiradi:

example.go
var wg sync.WaitGroup
for range 10 {
    wg.Go(func() {
        fmt.Print(".")
    })
}
wg.Wait()
text
..........

Barcha metodlar bir nechta goroutine'lar tomonidan parallel ishlatilganda ham mutlaqo xavfsizdir.

Odatda barcha Add chaqiruvlari Waitdan oldin amalga oshiriladi. Biroq, texnik jihatdan ba'zi Add chaqiruvlarini Waitdan oldin, ba'zilarini esa keyinroq (boshqa goroutine'dan) bajarishga hech narsa to'sqinlik qilmaydi.

Siz Waitni bir vaqtning o'zida bir nechta goroutine'dan chaqirishingiz mumkin. Guruh hisoblagichi nolga tenglashgunga qadar ularning barchasi bloklanib turadi.


Data races

Ma'lumotlar poygasi (data race) bir vaqtning o'zida bir nechta goroutine bitta umumiy ma'lumotga murojaat qilganda va ulardan kamida bittasi ma'lumotni o'zgartirganda sodir bo'ladi. Biz umumiy xotirani bunday parallel kirishdan himoya qilishimiz shart.

Ma'lumotlar poygasi har doim ham runtime panic xatosini keltirib chiqarmaydi. Shu sababli Go tili maxsus race detector (poyga aniqlovchi) vositasini taqdim etadi. Siz uni -race bayrog'i orqali yoqishingiz mumkin, u test, run, build va install buyruqlari bilan birga ishlaydi:

example.go
var total int

// There's a data race on total.
var wg sync.WaitGroup
wg.Go(func() { total++ })
wg.Go(func() { total++ })
wg.Wait()

fmt.Println("total:", total)
text
total: 2
example.go
go run -race main.go
text
==================
WARNING: DATA RACE
...
Found 1 data race(s)

Kanallar parallel o'qish va yozish uchun to'liq xavfsizdir va ular hech qachon ma'lumotlar poygasini keltirib chiqarmaydi.

Data race'larning oldini olish usullari:

  • Ma'lumotlarni parallel o'zgartirishdan qochish (odatda kanallardan foydalanish orqali).
  • Myutekslar yordamida kirishni sinxronlashtirish.
  • Faqat atomar operatsiyalardan foydalanish.

Race conditions

Poyga holati (race condition) bir nechta goroutine'lar tomonidan operatsiyalarning kutilmagan ketma-ketlikda bajarilishi tizimning noto'g'ri holatiga olib kelganda yuz beradi:

example.go
// There's a race condition when working with balance.
withdraw := func(amount int) {
    if getBalance() < amount {
        return
    }
    time.Sleep(time.Millisecond)
    setBalance(getBalance() - amount)
}

setBalance(50)

var wg sync.WaitGroup
wg.Go(func() { withdraw(40) })
wg.Go(func() { withdraw(40) })
wg.Wait()

fmt.Println("balance:", getBalance())
text
balance: -30

Agar alohida operatsiyalarning har biri parallel-xavfsiz bo'lsa, Go'ning race detector'i hech qanday xatolik topmaydi. Shu sababli u mantiqiy darajadagi poyga holatlarini (race conditions) aniqlay olmaydi:

example.go
go run -race main.go
text
balance: -30

Parallel muhitda noaniqlikni to'liq yo'qotib bo'lmaydi. Voqealar oldindan aytib bo'lmaydigan tartibda sodir bo'ladi — bu parallellikning tabiatidir. Biroq, siz butun bir kompozit operatsiyani myuteks yordamida himoya qilish orqali poyga holatining oldini olishingiz mumkin:

example.go
var mu sync.Mutex
withdraw := func(amount int) {
    mu.Lock()
    defer mu.Unlock()

    if getBalance() < amount {
        return
    }
    time.Sleep(time.Millisecond)
    setBalance(getBalance() - amount)
}

setBalance(50)

var wg sync.WaitGroup
wg.Go(func() { withdraw(40) })
wg.Go(func() { withdraw(40) })
wg.Wait()

fmt.Println("balance:", getBalance())
text
balance: 10

Compare-and-set

Ba'zan siz myutekslardan foydalanmasdan, atomar compare-and-set operatsiyasini yoki uning turlarini qo'llash orqali poyga holatining oldini olishingiz mumkin:

text
// CompareAndSet changes the value to new if the current value equals old.
// Returns true if the value was changed.
CompareAndSet(old, new any) bool

// CompareAndSwap changes the value to new if the current value equals old.
// Returns the old value.
CompareAndSwap(old, new any) any

// CompareAndDelete deletes the value if the current value equals old.
// Returns true if the value was deleted.
CompareAndDelete(old any) bool

// etc

Asosiy tamoyil har doim bir xil:

  • Faraz qilingan (eski) holat haqiqatga mos kelishini tekshiring.
  • Agar mos kelsa, holatni yangisiga o'zgartiring.
  • Agar mos kelmasa, hech narsa qilmang.

Mutexes

sync.Mutex turi umumiy ma'lumotlarni va kodingiz qismlarini bir vaqtning o'zida parallel bajarilishdan himoya qiladi:

example.go
var total int
var mu sync.Mutex

var wg sync.WaitGroup
for range 100 {
    wg.Go(func() {
        mu.Lock()
        time.Sleep(time.Millisecond)
        total++
        mu.Unlock()
    })
}
wg.Wait()
text
total: 100

Myuteks Lock() va Unlock() orasidagi kodni bir vaqtning o'zida faqat bitta goroutine bajara olishini kafolatlaydi.

Myuteks quyidagi hollarda qo'llaniladi:

  • Bir nechta goroutine bitta ma'lumotni o'zgartirayotganda.
  • Bitta goroutine ma'lumotni o'zgartirib, boshqalari uni o'qiyotganda.

Agar barcha goroutine'lar ma'lumotni faqat o'qiyotgan bo'lsa, sizga myuteks kerak emas.

TryLock

TryLock metodi xuddi oddiy Lock kabi myuteksni qulflashga urinadi. Biroq agar myuteks allaqachon band bo'lsa, u goroutine'ni bloklash o'rniga darhol false qaytaradi:

example.go
var total int
var mu sync.Mutex

var wg sync.WaitGroup
for range 100 {
    wg.Go(func() {
        if !mu.TryLock() {
            return
        }
        defer mu.Unlock()
        time.Sleep(time.Millisecond)
        total++
    })
}
wg.Wait()
text
total: 1

RWMutex

sync.RWMutex turi o'quvchilar (readers) va yozuvchilarni (writers) bir-biridan ajratadi. U ikkita alohida metodlar to'plamini taqdim etadi:

  • Lock / Unlock: myuteksni ham o'qish, ham yozish uchun to'liq qulflaydi va ochadi.
  • RLock / RUnlock: myuteksni faqat o'qish uchun qulflaydi va ochadi.
example.go
var total int
var mu sync.RWMutex
var wg sync.WaitGroup

// 10 writers.
for range 10 {
    wg.Go(func() {
        mu.Lock()
        defer mu.Unlock()
        time.Sleep(time.Millisecond)
        total++
    })
}

// 10 readers.
for range 10 {
    wg.Go(func() {
        // Try switching from RLock/RUnlock to Lock/Unlock
        // and see how it affects the elapsed time.
        mu.RLock()
        defer mu.RUnlock()
        time.Sleep(time.Millisecond)
        _ = total
    })
}

wg.Wait()
text
elapsed: 10ms

Uning ishlash mexanizmi:

  • Agar biror goroutine myuteksni Lock() bilan qulflasa, boshqa goroutine'lar Lock() yoki RLock() chaqirganda bloklanadi.
  • Agar goroutine myuteksni RLock() bilan qulflasa, boshqa o'quvchi goroutine'lar ham bloklanmasdan uni RLock() orqali qulflay oladi.
  • Agar kamida bitta goroutine myuteksni RLock() bilan ushlab turgan bo'lsa, yangi yozuvchi goroutine Lock() chaqirganda bloklanadi.

Bu "yagona yozuvchi, ko'p sonli o'quvchilar" (single writer, multiple readers) rejimini ta'minlaydi.

Locker

Ham sync.Mutex, ham sync.RWMutex bir xil sync.Locker interfeysini amalga oshiradi:

text
type Locker interface {
    Lock()
    Unlock()
}

Aniq bir myuteks turi o'rniga Locker interfeysidan foydalanish orqali siz aniq qulflash mexanizmiga bog'liq bo'lmagan modullarni qurishingiz mumkin. Bu foydalanuvchiga qaysi qulf turini ishlatishni o'zi tanlash imkonini beradi.

Channel as mutex

Umumiy ma'lumotlarni himoya qilish uchun myuteks o'rniga 1 ta sig'imli buferlangan kanaldan ham foydalanishingiz mumkin:

example.go
var total int
lock := make(chan struct{}, 1)

var wg sync.WaitGroup
wg.Go(func() {
    lock <- struct{}{}
    defer func() { <-lock }()
    total++
})
wg.Go(func() {
    lock <- struct{}{}
    defer func() { <-lock }()
    total++
})
wg.Wait()
text
total: 2

Semaphores

Semafor — bu N ta mavjud uyaga (slot) va ikkita operatsiyaga ega bo'lgan konteynerdir: slotni egallash uchun acquire va uni bo'shatish uchun release. Semafor qoidalari quyidagicha:

  • acquire chaqiruvi bo'sh slotni band qiladi.
  • Agar bo'sh slotlar bo'lmasa, acquire chaqirgan goroutine'ni bloklaydi.
  • release chaqiruvi band qilingan slotni bo'shatadi.
  • Agar release chaqirilganda acquireda bloklangan goroutine'lar bo'lsa, ulardan biri bo'shagan slotni darhol egallaydi va blokdan chiqadi.

Siz oddiy semaforni buferlangan kanal yordamida amalga oshirishingiz mumkin, bunda N kanalning bufer hajmi hisoblanadi. Semaforni band qilish uchun kanalga qiymat yuborasiz. Uni bo'shatish uchun esa kanaldan qiymatni o'qib olasiz:

example.go
// Try changing nConc and see how the elapsed time changes.
const nConc = 4
const nCalls = 100
sema := make(chan struct{}, nConc)

var wg sync.WaitGroup
for range nCalls {
    sema <- struct{}{} // acquire
    wg.Go(func() {
        defer func() { <-sema }() // release
        time.Sleep(time.Millisecond) // do some work
    })
}
wg.Wait()
text
elapsed: 25ms

Keyingi murakkabroq vaziyatlar uchun rasmiy golang.org/x/sync/semaphore paketidan foydalanish tavsiya etiladi.

Rendezvous

Rendezvous (uchrashuv nuqtasi) ikkita goroutine'ga bir-birini kutish imkonini beradi:

  • Ikkita goroutine mavjud — G1 va G2 — va har biri o'zining tayyorligi haqida signal bera oladi.
  • Agar G1 signal bersa, lekin G2 hali signal bermagan bo'lsa, G1 bloklanadi va kutadi.
  • Agar G2 signal bersa, lekin G1 hali signal bermagan bo'lsa, G2 bloklanadi va kutadi.
  • Har ikkala goroutine signal berib bo'lgach, ularning ikkalasi ham blokdan chiqadi va o'z ishini davom ettiradi.

Oddiy rendezvous'ni wait group yordamida amalga oshirish mumkin:

example.go
var rend sync.WaitGroup
rend.Add(2)

var wg sync.WaitGroup
wg.Go(func() {
    fmt.Println("before rendezvous")
    rend.Done()
    rend.Wait()
    fmt.Println("after rendezvous")
})
wg.Go(func() {
    fmt.Println("before rendezvous")
    rend.Done()
    rend.Wait()
    fmt.Println("after rendezvous")
})
wg.Wait()
text
before rendezvous
before rendezvous
after rendezvous
after rendezvous

Barrier

Barrier (to'siq) — bu rendezvous'ning umumiy holatidir. U N ta goroutine'ga bir-birini kutish imkonini beradi:

  • To'siq hisoblagichga (0 dan boshlanadi) va N chegarasiga ega.
  • To'siqqa yetib kelgan har bir goroutine hisoblagichni 1 taga oshiradi.
  • To'siq unga yetib kelgan har qanday goroutine'ni bloklaydi.
  • Hisoblagich N ga yetishi bilan to'siq barcha kutayotgan goroutine'larni birdaniga blokdan chiqaradi.

Oddiy to'siqni wait group yordamida amalga oshirish mumkin:

example.go
const n = 4
var bar sync.WaitGroup
bar.Add(n)

var wg sync.WaitGroup
for range n {
    wg.Go(func() {
        fmt.Println("before the barrier")
        bar.Done()
        bar.Wait()
        fmt.Println("after the barrier")
    })
}
wg.Wait()
text
before the barrier
before the barrier
before the barrier
before the barrier
after the barrier
after the barrier
after the barrier
after the barrier

Signaling

sync.Cond (shartli o'zgaruvchi - conditional variable) turi bir goroutine boshqasiga o'zining tayyorligini bildirishiga va ikkinchi goroutine ushbu signalni kutishiga imkon beradi.

Cond o'z ichiga myuteksni oladi va ikkita asosiy metodga ega — Wait va Signal:

  • Wait myuteksni ochadi va signal kelgunga qadar goroutine bajarilishini to'xtatib turadi.
  • Signal Waitda kutayotgan bitta goroutine'ni uyg'otadi.
  • Wait uyg'onganda, u myuteksni qaytadan qulflaydi.
example.go
cond := sync.NewCond(&sync.Mutex{})
done := false

var wg sync.WaitGroup
wg.Go(func() {
    cond.L.Lock()
    fmt.Println("G1 is ready to signal")
    done = true
    cond.Signal()
    cond.L.Unlock()
})
wg.Go(func() {
    cond.L.Lock()
    for !done {
        cond.Wait()
    }
    fmt.Println("G2 received the signal")
    cond.L.Unlock()
})
wg.Wait()
text
G1 is ready to signal
G2 received the signal

Agar Signal chaqirilganda bir nechta kutayotgan goroutine bo'lsa, ulardan faqat bittasi uyg'otiladi. Agar kutayotgan goroutine bo'lmasa, Signal hech narsa qilmaydi.

Shuningdek, Broadcast metodidan ham foydalanish mumkin. Signal faqat bitta goroutine'ni uyg'otsa, Broadcast metodi Cond.Waitda kutayotgan barcha goroutine'larni baravariga uyg'otadi.

Kanal yordamida ham signal yuborishingiz mumkin:

example.go
signal := make(chan struct{}, 1)

go func() {
    // do something
    signal <- struct{}{}
}()

go func() {
    <-signal
    // do something
}()

Shuningdek, ommaviy xabar (broadcast) ham yuborish mumkin:

example.go
broadcast := make(chan struct{})

go func() {
    // do something
    close(broadcast)
}()

go func() {
    <-broadcast
    // do something
}()

go func() {
    <-broadcast
    // do something
}()

Shartli o'zgaruvchi (Cond) yordamida ommaviy xabar yuborish ma'lum cheklovlarga ega: u haqiqiy ma'lumotlarni emas, balki faqat signalni uzatadi va faqat bir marta ishlaydi. Kanallar yordamida esa bunday cheklovlarsiz to'liq publish/subscribe (nashr qilish va obuna bo'lish) tizimini qurishingiz mumkin:

example.go
type Publisher struct {
    sbox []chan int // subscription channels
    mu   sync.Mutex // protects the state
}

func (p *Publisher) Subscribe() <-chan int {
    p.mu.Lock()
    defer p.mu.Unlock()
    sub := make(chan int, 1)
    p.sbox = append(p.sbox, sub)
    return sub
}

func (p *Publisher) Broadcast(v int) {
    p.mu.Lock()
    defer p.mu.Unlock()
    for _, sub := range p.sbox {
        select {
        case sub <- v:
        default:
        }
    }
}

Run once

sync.Once turi berilgan funksiya faqat bir marta bajarilishini qat'iy ta'minlaydi. Agar bir nechta goroutine bir vaqtning o'zida Once.Doni chaqirsa, ulardan faqat bittasi funksiyani bajaradi, boshqalari esa u qaytguncha kutib turadi:

example.go
total := 0
initState := func() {
    total += 1
}

var once sync.Once

var wg sync.WaitGroup
wg.Go(func() {
    once.Do(initState)
    // do something
})
wg.Go(func() {
    once.Do(initState)
    // do something
})
wg.Wait()
text
total: 1

Once parallel muhitda bir martalik xavfsiz initsializatsiya yoki resurslarni tozalash uchun ayni muddao hisoblanadi.

Once turidan tashqari, sync paketi uchta qulay yordamchi funksiyalarni ham taqdim etadi:

example.go
// Calls f only once.
func (o *Once) Do(f func())

// Returns a function that calls f only once.
func OnceFunc(f func()) func()

// Returns a function that calls f only once
// and returns the value from that first call.
func OnceValue[T any](f func() T) func() T

// Returns a function that calls f only once
// and returns the pair of values from that first call.
func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2)

Object pool

sync.Pool turi xotirani har safar qaytadan ajratish (allocate) o'rniga obyektlarni qayta ishlatishga yordam beradi, bu esa Go axlat yig'uvchisiga (garbage collector) tushadigan yukni keskin kamaytiradi:

example.go
pool := sync.Pool{
    New: func() any {
        buf := make([]byte, 1024)
        return &buf
    },
}

// Only allocates 4*1024 B, despite 4000 loop iterations.
var wg sync.WaitGroup
for range 4 {
    wg.Go(func() {
        for range 1000 {
            buf := pool.Get().(*[]byte)
            sink = buf
            pool.Put(buf)
        }
    })
}
wg.Wait()
text
Memory allocated: 4 KB

Get havzadan tayyor obyektni oladi. Agar havzada bo'sh obyekt mavjud bo'lmasa, u New orqali yangisini yaratadi (buni biz o'zimiz yozishimiz kerak, chunki havza o'zi saqlayotgan obyektlar haqida oldindan bilmaydi). Put ishlatib bo'lingan obyektni yana havzaga qaytaradi.

Doimo yodda tutish kerak bo'lgan muhim qoidalar:

  • New xotirani nusxalashni kamaytirish va keraksiz allokatsiyalarning oldini olish uchun qiymat emas, balki ko'rsatkich (pointer) qaytarishi lozim.
  • Havzaning o'lcham chegarasi yo'q. Agar bir vaqtning o'zida Get chaqiruvchi yana 1000 ta goroutine ishga tushirsangiz, yana 1000 ta bufer ajratiladi.
  • Obyekt Put orqali havzaga qaytarilgach, undan boshqa foydalanmasligingiz kerak (chunki boshqa goroutine uni darhol havzadan olib ishlatayotgan bo'lishi mumkin).

Atomics

Sinxronizatsiyasiz operatsiya faqat bitta protsessor buyrug'iga (CPU instruction) tarjima qilingandagina chinakam atomar bo'la oladi. Bunday operatsiyalar qulflarga (lock) muhtoj emas va parallel chaqirilganda hech qachon muammo tug'dirmaydi (hatto yozish operatsiyalari ham).

Atomar turlar sanoqli bo'lib, ularning barchasi sync/atomic paketida joylashgan:

text
Int32     Bool
Int64     Value
Uint32    Pointer
Uint64

Har bir atomar tur quyidagi asosiy metodlarni taqdim etadi:

  • Load: o'zgaruvchining qiymatini xavfsiz o'qiydi.
  • Store: yangi qiymat o'rnatadi.
  • Swap: yangi qiymat o'rnatadi (Store kabi) va eski qiymatni qaytaradi.
  • CompareAndSwap: faqat joriy qiymat kutilgan eski qiymatga teng bo'lsagina yangi qiymatni o'rnatadi.
example.go
var n atomic.Int32
n.Store(10)
swapped := n.CompareAndSwap(10, 42)
fmt.Println("CompareAndSwap 10 -> 42:", swapped)
fmt.Println("n =", n.Load())
text
CompareAndSwap 10 -> 42: true
n = 42

Raqamli turlar, shuningdek, qiymatni ko'rsatilgan songa oshiruvchi Add metodini ham taqdim etadi.

Barcha metodlar yo bitta CPU buyrug'iga tarjima qilinadi yoki atomarligi boshqa yo'l bilan qat'iy kafolatlanadi, shuning uchun ularni bir nechta goroutine'larda qo'llash to'liq xavfsizdir.

Biroq, atomar operatsiyalarning birikmasi (kompozitsiyasi) har doim noatomardir:

example.go
var delta atomic.Int32
var counter atomic.Int32

func increment() {
    // Not atomic; causes a race condition.
    delta.Add(1)
    sleep(10)
    counter.Add(delta.Load())
}

// After 100 concurrent increments,
// the final value is NOT guaranteed.
text
counter = 9386

Murakkab operatsiyani atomar qilish va poyga holatlarining oldini olishning eng ishonchli usuli — myuteksdan foydalanishdir:

example.go
var delta int32
var counter int32
var mu sync.Mutex

func increment() {
    // Atomic; doesn't cause a race condition.
    mu.Lock()
    delta += 1
    sleep(10)
    counter += delta
    mu.Unlock()
}

// After 100 concurrent increments, the final value is guaranteed:
// counter = 1+2+...+100 = 5050
text
counter = 5050

Ba'zan vazifadan muddatidan oldin chiqish (exit early) uchun myuteks o'rniga atomar turdan foydalanishingiz mumkin:

example.go
type Gate struct {
    closed atomic.Bool
}

func (g *Gate) Close() {
    if !g.closed.CompareAndSwap(false, true) {
        return // ignore repeated calls
    }
    // The gate is closed.
    // We can free resources now.
}

Testing

Agar sizning parallel dasturingiz kanallar yoki Wait kabi sinxronizatsiya metodlariga ega maxsus turlardan foydalansa, ularni testlaringizda bemalol qo'llashingiz mumkin. Shu tariqa, testlaringiz kod sinxron bo'lganidagidan unchalik murakkab bo'lmaydi:

example.go
// Calc calculates something asynchronously.
func Calc() <-chan int {
    out := make(chan int, 1)
    go func() {
        out <- 42
    }()
    return out
}
example.go
func Test(t *testing.T) {
    // Wait for the Calc goroutine to finish.
    got := <-Calc()
    if got != 42 {
        t.Errorf("got: %v; want: 42", got)
    }
}
text
PASS

Agar siz test qilayotgan kodda qulay sinxronizatsiya dastaklari bo'lmasa, synctest paketidan foydalanishingiz mumkin. U ikkita funksiyani taqdim etadi:

example.go
func Test(t *testing.T, f func(*testing.T))
func Wait()

synctest.Test izolyatsiyalangan sinov muhitini (bubble) ishga tushiradi. Ushbu muhit soxta soatdan (fake clock) foydalanadi va siz goroutine'lar sinxronizatsiyasini synctest.Wait orqali qo'lda boshqarishingiz mumkin.

synctest.Wait ushbu muhitdagi barcha goroutine'lar — Waitni chaqirgan goroutine'dan tashqari — yo o'z ishini tugatguncha, yoki barqaror bloklanguncha (durably blocked) kutib turadi. Bu ma'lum bir goroutine tugashini yoki bloklanishini kutib, dastur holatini xavfsiz tekshirish imkonini beradi:

example.go
// NewProc starts the calculation.
func NewProc() *Proc {
    p := &Proc{done: make(chan struct{})}
    go func() {
        p.res = 42
        <-p.done // (X)
        p.res = 0
    }()
    return p
}
example.go
func Test(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        p := NewProc()
        defer p.Stop()

        // Wait for the goroutine to block at point X.
        synctest.Wait()
        if got := p.Res(); got != 42 {
            t.Fatalf("got %v, want 42", got)
        }
    })
}
text
PASS

synctest.Testdagi soxta soat faqat quyidagi uchta shart bir vaqtda bajarilgandagina oldinga siljiydi:

  1. Muhitdagi barcha goroutine'lar barqaror bloklangan bo'lsa;
  2. Kelajakda kamida bitta goroutine blokdan chiqadigan aniq bir vaqt mavjud bo'lsa;
  3. Ayni damda synctest.Wait ishlamayotgan bo'lsa.

Shu tufayli vaqtga bog'liq barcha testlar bir zumda (instantly) bajariladi:

example.go
// Calc processes a value from the input channel.
// Times out if no input is received after 3 seconds.
func Calc(in chan int) (int, error) {
    select {
    case v := <-in:
        return v * 2, nil
    case <-time.After(3 * time.Second):
        return 0, ErrTimeout
    }
}
example.go
func Test(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        ch := make(chan int)
        got, err := Calc(ch) // runs instantly

        if err != ErrTimeout {
            t.Errorf("got: %v; want: %v", err, ErrTimeout)
        }
        if got != 0 {
            t.Errorf("got: %v; want: 0", got)
        }
    })
}
text
PASS

Quyidagi operatsiyalar goroutine'ni barqaror bloklaydi (durably block):

  • Muhit ichida yaratilgan kanalda bloklovchi yuborish yoki qabul qilish.
  • Har bir case ushbu muhit ichida yaratilgan kanal bo'lgan bloklovchi select operatori.
  • Cond.Wait chaqiruvi.
  • Agar barcha WaitGroup.Add chaqiruvlari muhit ichida amalga oshirilgan bo'lsa, WaitGroup.Wait chaqiruvi.
  • time.Sleep chaqiruvi.

Myutekslar, kiritish-chiqarish (I/O) yoki tizim chaqiruvlarida (syscalls) bloklanish barqaror hisoblanmaydi va synctest muhiti ularni boshqara olmaydi.


Scheduling

Uskuna darajasida (hardware level) parallel vazifalarni bajarish uchun protsessor yadrolari (CPU cores) javobgardir.

Operatsion tizim darajasida bajarilishning asosiy birligi — bu operatsion tizim oqimi (OS thread) hisoblanadi. Odatda CPU yadrolariga nisbatan tizim oqimlari ancha ko'p bo'ladi, shuning uchun operatsion tizimning rejalashtiruvchisi qaysi oqimlarni ishlatish va qaysilarini to'xtatib turishni hal qiladi.

Go runtime darajasida esa bajarilishning asosiy birligi — goroutine hisoblanadi. Runtime rejalashtiruvchisi belgilangan miqdordagi OS oqimlarini (odatda bitta CPU yadrosiga bitta oqim) boshqaradi. Tizim oqimlariga nisbatan goroutine'lar soni juda ko'p bo'lishi mumkin, shuning uchun Go scheduler mavjud oqimlarda qaysi goroutine'larni ishga tushirish va qaysilarini to'xtatib turishni o'zi hal qiladi. Rejalashtiruvchi har bir goroutine navbatda cheksiz kutmasdan o'z navbatini olishini ta'minlash uchun goroutine'lar o'rtasida uzluksiz almashinib turadi.

text
CPU                  OS                   Go runtime
┌──────────┐  run on ┌──────────┐  run on ┌────────────┐
│ Cores    │ <────── │ Threads  │ <────── │ Goroutines │
└──────────┘         └──────────┘         └────────────┘

Go parallellikni aynan shu tarzda samarali boshqaradi.

Goroutine scheduler

Goroutine rejalashtiruvchisining vazifasi — M ta goroutine'ni N ta operatsion tizim oqimida ishga tushirishdir, bunda M odatda N dan bir necha barobar katta bo'ladi. Uning algoritmining soddalashtirilgan tartibi:

  • Agar bo'sh oqim bo'lsa, unga umumiy navbatdan navbatdagi goroutine'ni tayinlang.
  • Agar ishlayotgan goroutine bloklanib qolsa (masalan, kanaldan o'qish paytida), uni yana navbatga qaytaring va oqimga boshqa tayyor goroutine'ni tayinlang.
  • Agar ishlayotgan goroutine tizim chaqiruvida (syscall) tiqilib qolsa, bloklangan goroutine syscall'ni yakunlaguncha boshqa goroutine'larni bajarish uchun yangi tizim oqimini (OS thread) ishga tushiring.
  • Ishlayotgan goroutine'larni har 10 ms da tekshirib turing. Ochlikning (starvation) oldini olish uchun uzoq vaqt ishlayotgan goroutine'larni majburan to'xtating (preempt) va ularni navbatga qaytaring.
text
┌─────┐┌─────┐┌─────┐┌─────┐
│ G17 ││ G18 ││ G19 ││ G20 │                        queue
└─────┘└─────┘└─────┘└─────┘

┌─────┐      ┌─────┐      ┌─────┐      ┌─────┐
│ G15 │      │ G16 │      │ G13 │      │ G14 │      running
└─────┘      └─────┘      └─────┘      └─────┘
  │            │            │            │
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Thread E │ │ Thread F │ │ Thread C │ │ Thread D │
└──────────┘ └──────────┘ └──────────┘ └──────────┘

┌─────┐      ┌─────┐
│ G11 │      │ G12 │                                syscalls
└─────┘      └─────┘
  │            │
┌──────────┐ ┌──────────┐
│ Thread A │ │ Thread B │
└──────────┘ └──────────┘

Go kodini bajarayotgan oqimlar soni GOMAXPROCS muhit o'zgaruvchisi yoki runtime.GOMAXPROCS funksiyasi orqali boshqariladi.

Goroutine — bu dastlab taxminan 2 KB xotiradan (asosan uning steki uchun) foydalanadigan juda yengil tuzilmadir. Zarur bo'lganda stek dinamik ravishda o'sishi mumkin. Goroutine'lar shunchalik yengil bo'lgani bois, siz oddiy bir kompyuterda o'n minglab yoki yuz minglab goroutine'larni bir vaqtda bemalol ishlatishingiz mumkin.


Diagnostics

Production muhitida parallel dasturlardagi muammolarni aniqlash va bartaraf etish uchun biz metrikalar, profillash (profiling) va kuzatuvdan (tracing) foydalanamiz.

Metrikalar (Metrics) Go runtime'ning qanday ishlayotganini ko'rsatadi, masalan, heap xotirasidan qancha foydalanilayotgani yoki axlat yig'ish (garbage collection) to'xtalishlari qancha vaqt olayotgani. Har bir metrika o'zining yagona nomiga va son yoki gistogramma ko'rinishidagi qiymatiga ega.

Metrikalarning to'liq ro'yxatini olish yoki aniq birining qiymatini tekshirish uchun runtime/metrics paketidan foydalanishingiz mumkin:

example.go
samples := []metrics.Sample{
    {Name: "/sched/gomaxprocs:threads"},
    {Name: "/sched/goroutines:goroutines"},
}
metrics.Read(samples)

for _, s := range samples {
    fmt.Printf("%s: %v\n", s.Name, s.Value.Uint64())
}
text
/sched/gomaxprocs:threads: 8
/sched/goroutines:goroutines: 1

Amaliyotda buni kamdan-kam hollarda qo'lda bajaradilar. Odatda barcha metrikalar Prometheus yoki OpenTelemetry kutubxonalari yordamida avtomatik tarzda monitoring tizimlariga eksport qilinadi.

Profillash (Profiling) dastur aynan nima qilayotganini, qanday resurslarni sarflayotganini va bu kodning qaysi joyida yuz berayotganini chuqur tushunishga yordam beradi. Go production muhitiga mos keladigan namuna oluvchi profaylerdan (sampling profiler) foydalanadi.

Eng ko'p qo'llaniladigan profillar: CPU (har bir funksiya qancha protsessor vaqtini olayotganini ko'rsatadi) va Heap (har bir funksiya qancha xotira ajratayotganini ko'rsatadi). Goroutine, Block va Mutex profillari esa parallellik bilan bog'liq muammolarni (tiqilishlar, qulflanishlar) aniqlashga yordam beradi.

Ilovangizga profayler qo'shishning eng oson yo'li — net/http/pprof paketidan foydalanishdir. Berilgan nomdagi profilni yig'ish uchun /debug/pprof/{name} endpoint'iga murojaat qiling. Yig'ilgan profilni tahlil qilish uchun esa go tool pprof utilitasidan foydalaning:

bash
go tool pprof -proto \
  "http://localhost:6060/debug/pprof/profile?seconds=N" > cpu.pprof
go tool pprof -http=localhost:8080 cpu.pprof

Shuningdek, profillashni qo'lda ham boshqarishingiz mumkin:

example.go
// CPU profile.
file, _ := os.Create("cpu.prof")
defer file.Close()
pprof.StartCPUProfile(file)
defer pprof.StopCPUProfile()
// ...
example.go
// Any other profile.
file, _ := os.Create(name + ".prof")
defer file.Close()
pprof.Lookup(name).WriteTo(file, 0)

Kuzatuv (Tracing) dastur ishlayotgan vaqtda yuz beradigan ma'lum voqealarni, asosan parallellik va xotira bilan bog'liq bo'lgan hodisalarni batafsil yozib boradi. net/http/pprof paketidagi server ishlab turganda, trace yig'ish uchun /debug/pprof/trace endpoint'ini chaqiring. Natijalarni vizual tahlil qilish uchun go tool trace utilitasidan foydalaning.

Shuningdek, trace'ni qo'lda ham yozib olishingiz mumkin:

example.go
file, _ := os.Create("trace.out")
defer file.Close()
trace.Start(file)
defer trace.Stop()
// ...

Hajm yoki muddat bilan cheklangan siljuvchi oyna (sliding window) yordamida avtomatik kuzatuvni sozlashingiz mumkin. Bu "flight recording" (samolyot qora qutisi) deb ataladi. U biron kutilmagan nosozlik yuz bergan taqdirda tahlil qilish uchun doimo eng so'nggi hodisalar tarixini saqlab turish imkonini beradi:

example.go
cfg := trace.FlightRecorderConfig{
    MinAge:   5 * time.Second,
    MaxBytes: 3 << 20, // 3MB
}
rec := trace.NewFlightRecorder(cfg)
rec.Start()
defer rec.Stop()

Xulosa

Biz Go tilida parallel dasturlash uchun mo'ljallangan quyidagi asosiy vosita va mexanizmlarni to'liq ko'rib chiqdik:

  • Goroutines: Parallel vazifalarni bajarish uchun o'ta yengil oqimlar.
  • Channels va Select: Goroutine'lar o'rtasida moslashuvchan, xavfsiz aloqa va ma'lumotlar oqimini boshqarish.
  • Timers va Tickers: Vaqt chegaralari (timeout) va davriy vazifalar bilan ishlash.
  • Context: Operatsiyalarni butun daraxt bo'ylab bekor qilish va muddatlarni nazorat qilish.
  • Wait groups: Goroutine'larni o'zaro sinxronlashtirish va ularning tugashini kutish.
  • Mutexes: Poyga holatlari va ma'lumotlar to'qnashuvining (data races) oldini olish.
  • Condition variables: Hodisalar haqida signallar berish va uyg'otish.
  • Once: Xavfsiz bir martalik initsializatsiya.
  • Pools: Axlat yig'uvchiga tushadigan yukni kamaytirish uchun xotira obyektlarini qayta ishlatish.
  • Atomic operatsiyalar: Qulflarsiz (lock-free) xavfsiz past darajadagi amallar.
  • Testing: Parallel dasturlarni ishonchli va deterministik sinovdan o'tkazish.
  • Scheduling va Diagnostics: Go runtime GMP modeli, CPU/xotira profillash va trace orqali diagnostika qilish.

Ushbu vositalar Go tilini yuqori yuklamali, kengayuvchan va ishonchli tizimlar yaratishda dunyodagi eng ilg'or dasturlash tillaridan biriga aylantiradi.