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.
Go tilida parallellikning poydevori — bu goroutinelar (gorutinalar) bo'lib, ular go kalit so'zi yordamida ishga tushiriladigan funksiyalardir:
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()
}
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:
func main() {
var wg sync.WaitGroup
wg.Go(func() {
fmt.Println("worker 1")
})
wg.Go(func() {
fmt.Println("worker 2")
})
wg.Wait()
}
worker 2
worker 1
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:
func main() {
messages := make(chan string)
go func() { messages <- "ping" }()
msg := <-messages
fmt.Println(msg)
}
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.
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:
func generate(start, stop int) chan int {
out := make(chan int)
go func() {
for i := start; i < stop; i++ {
out <- i
}
}()
return out
}
Barcha ma'lumotlar jo'natib bo'linganligini qabul qiluvchilarga (o'quvchilarga) bildirish uchun yozuvchi goroutine kanalni close() orqali yopadi:
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:
func main() {
in := generate(5, 10)
for {
num, ok := <-in
if !ok {
break
}
fmt.Print(num, " ")
}
}
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.
range operatori avtomatik ravishda kanaldan navbatdagi qiymatni o'qiydi va kanal yopilganligini tekshiradi. Agar kanal yopilgan bo'lsa, u tsikldan o'zi chiqadi:
func main() {
nums := generate(5, 10)
for n := range nums {
fmt.Print(n, " ")
}
}
5 6 7 8 9
Slice bo'yicha range'dan farqli o'laroq, kanal ustida range juftlik emas, balki bitta yagona qiymat qaytaradi.
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:
stream := make(chan int)
go func(in chan<- int) {
in <- 42
}(stream)
func(out <-chan int) {
fmt.Println(<-out)
}(stream)
42
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:
stream := make(chan int, 3)
stream <- 11
stream <- 12
stream <- 13
fmt.Println(<-stream)
fmt.Println(<-stream)
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:
stream := make(chan int, 3)
stream <- 11
fmt.Println(cap(stream), len(stream))
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:
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
11 true
0 false
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:
var stream chan int
go func() {
// blocks forever
stream <- 1
}()
// blocks forever
<-stream
nil kanalni yopish panic chaqiradi:
var stream chan int
close(stream)
// panic: close of nil channel
select operatori qaysidir ma'noda switchga o'xshaydi, lekin u maxsus kanallar bilan ishlash uchun loyihalashtirilgan. Uning ishlash tartibi:
caselar bloklanmaganligini tekshiradi.case tayyor bo'lsa, ulardan birini tasodifiy (randomly) tanlab bajaradi.caselar bloklangan bo'lsa va default mavjud bo'lsa, defaultni bajaradi.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:
// 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)
10 11 20 12 21 22
Goroutine'larni bekor qilish (cancel) uchun:
// 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)
110
120
canceled
Bloklanmaydigan (non-blocking) operatsiyalar uchun:
// 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
}
110 <nil>
120 <nil>
busy
Va yana ko'plab boshqa vazifalar uchun keng qo'llaniladi.
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:
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
}
Goroutine o'z ishini tugatganini boshqa goroutine'larga chiqish kanali orqali bildirishi mumkin:
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, " ")
}
}
5 6 7 8 9
Agar goroutine natija qaytarishi shart bo'lmasa, u o'z vazifasini yakunlaganligini done kanali orqali bildiradi:
func work() <-chan struct{} {
done := make(chan struct{})
go func() {
defer close(done)
fmt.Println("work done")
}()
return done
}
func main() {
done := work()
<-done
}
work done
Goroutine ishini muddatidan oldin to'xtatish uchun chaqiruvchi goroutine cancel kanalidan foydalanishi mumkin:
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)
}
1
2
3
Parallel konveyerlarda xatoliklarni boshqarishning uchta asosiy yondashuvi mavjud:
➊ Birinchi xatolikda darhol to'xtash (Return on the first error):
// 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:
// 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):
// 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
}
Sana va vaqt bilan ishlashdan tashqari, time paketi parallel dasturlarda vaqtga bog'liq bo'lgan operatsiyalarni boshqarish uchun juda qulay vositalarni taqdim etadi.
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:
// 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:
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)
}
work done
err = <nil>
err = timeout
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:
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
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:
// 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")
}
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:
done := make(chan struct{})
work := func() {
fmt.Println("work done")
close(done)
}
// executes work after 50ms
time.AfterFunc(50*time.Millisecond, work)
<-done
work done
time.AfterFunc() taymer obyektini qaytaradi, shuning uchun uni ish boshlanishidan oldin bekor qilish imkoni mavjud:
// 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")
}
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:
// 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.
Memory used: 4 KB, # allocations: 6
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:
// 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()
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).
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:
// 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):
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
}
err = context canceled
Timeout orqali bekor qilish (context.DeadlineExceeded xatosi):
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
}
err = context deadline exceeded
Deadline (aniq vaqt) orqali bekor qilish (context.DeadlineExceeded xatosi):
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
}
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:
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)
}
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:
ctx, cancel := context.WithCancelCause(context.Background())
cancel(errors.New("the night is dark"))
fmt.Println(context.Cause(ctx))
the night is dark
Kontekst bekor qilinganda bajariladigan maxsus tozalash funksiyasini context.AfterFunc() yordamida ro'yxatdan o'tkazishingiz mumkin:
ctx, cancel := context.WithCancel(context.Background())
cleanup := func() { fmt.Println("cleanup") }
context.AfterFunc(ctx, cleanup)
cancel()
time.Sleep(10 * time.Millisecond)
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.
sync.WaitGroup turi bir yoki bir nechta goroutine'larning ishini yakunlashini kutish imkonini beradi:
const n = 10
var wg sync.WaitGroup
wg.Add(n)
for range n {
go func() {
defer wg.Done()
fmt.Print(".")
}()
}
wg.Wait()
..........
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:
var wg sync.WaitGroup
for range 10 {
wg.Go(func() {
fmt.Print(".")
})
}
wg.Wait()
..........
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.
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:
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)
total: 2
go run -race main.go
==================
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:
Poyga holati (race condition) bir nechta goroutine'lar tomonidan operatsiyalarning kutilmagan ketma-ketlikda bajarilishi tizimning noto'g'ri holatiga olib kelganda yuz beradi:
// 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())
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:
go run -race main.go
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:
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())
balance: 10
Ba'zan siz myutekslardan foydalanmasdan, atomar compare-and-set operatsiyasini yoki uning turlarini qo'llash orqali poyga holatining oldini olishingiz mumkin:
// 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:
sync.Mutex turi umumiy ma'lumotlarni va kodingiz qismlarini bir vaqtning o'zida parallel bajarilishdan himoya qiladi:
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()
total: 100
Myuteks Lock() va Unlock() orasidagi kodni bir vaqtning o'zida faqat bitta goroutine bajara olishini kafolatlaydi.
Myuteks quyidagi hollarda qo'llaniladi:
Agar barcha goroutine'lar ma'lumotni faqat o'qiyotgan bo'lsa, sizga myuteks kerak emas.
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:
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()
total: 1
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.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()
elapsed: 10ms
Uning ishlash mexanizmi:
Lock() bilan qulflasa, boshqa goroutine'lar Lock() yoki RLock() chaqirganda bloklanadi.RLock() bilan qulflasa, boshqa o'quvchi goroutine'lar ham bloklanmasdan uni RLock() orqali qulflay oladi.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.
Ham sync.Mutex, ham sync.RWMutex bir xil sync.Locker interfeysini amalga oshiradi:
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.
Umumiy ma'lumotlarni himoya qilish uchun myuteks o'rniga 1 ta sig'imli buferlangan kanaldan ham foydalanishingiz mumkin:
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()
total: 2
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.acquire chaqirgan goroutine'ni bloklaydi.release chaqiruvi band qilingan slotni bo'shatadi.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:
// 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()
elapsed: 25ms
Keyingi murakkabroq vaziyatlar uchun rasmiy golang.org/x/sync/semaphore paketidan foydalanish tavsiya etiladi.
Rendezvous (uchrashuv nuqtasi) ikkita goroutine'ga bir-birini kutish imkonini beradi:
Oddiy rendezvous'ni wait group yordamida amalga oshirish mumkin:
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()
before rendezvous
before rendezvous
after rendezvous
after rendezvous
Barrier (to'siq) — bu rendezvous'ning umumiy holatidir. U N ta goroutine'ga bir-birini kutish imkonini beradi:
Oddiy to'siqni wait group yordamida amalga oshirish mumkin:
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()
before the barrier
before the barrier
before the barrier
before the barrier
after the barrier
after the barrier
after the barrier
after the barrier
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.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()
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:
signal := make(chan struct{}, 1)
go func() {
// do something
signal <- struct{}{}
}()
go func() {
<-signal
// do something
}()
Shuningdek, ommaviy xabar (broadcast) ham yuborish mumkin:
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:
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:
}
}
}
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:
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()
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:
// 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)
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:
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()
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.Get chaqiruvchi yana 1000 ta goroutine ishga tushirsangiz, yana 1000 ta bufer ajratiladi.Put orqali havzaga qaytarilgach, undan boshqa foydalanmasligingiz kerak (chunki boshqa goroutine uni darhol havzadan olib ishlatayotgan bo'lishi mumkin).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:
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.var n atomic.Int32
n.Store(10)
swapped := n.CompareAndSwap(10, 42)
fmt.Println("CompareAndSwap 10 -> 42:", swapped)
fmt.Println("n =", n.Load())
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:
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.
counter = 9386
Murakkab operatsiyani atomar qilish va poyga holatlarining oldini olishning eng ishonchli usuli — myuteksdan foydalanishdir:
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
counter = 5050
Ba'zan vazifadan muddatidan oldin chiqish (exit early) uchun myuteks o'rniga atomar turdan foydalanishingiz mumkin:
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.
}
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:
// Calc calculates something asynchronously.
func Calc() <-chan int {
out := make(chan int, 1)
go func() {
out <- 42
}()
return out
}
func Test(t *testing.T) {
// Wait for the Calc goroutine to finish.
got := <-Calc()
if got != 42 {
t.Errorf("got: %v; want: 42", got)
}
}
PASS
Agar siz test qilayotgan kodda qulay sinxronizatsiya dastaklari bo'lmasa, synctest paketidan foydalanishingiz mumkin. U ikkita funksiyani taqdim etadi:
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:
// 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
}
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)
}
})
}
PASS
synctest.Testdagi soxta soat faqat quyidagi uchta shart bir vaqtda bajarilgandagina oldinga siljiydi:
synctest.Wait ishlamayotgan bo'lsa.Shu tufayli vaqtga bog'liq barcha testlar bir zumda (instantly) bajariladi:
// 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
}
}
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)
}
})
}
PASS
Quyidagi operatsiyalar goroutine'ni barqaror bloklaydi (durably block):
case ushbu muhit ichida yaratilgan kanal bo'lgan bloklovchi select operatori.Cond.Wait chaqiruvi.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.
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.
CPU OS Go runtime
┌──────────┐ run on ┌──────────┐ run on ┌────────────┐
│ Cores │ <────── │ Threads │ <────── │ Goroutines │
└──────────┘ └──────────┘ └────────────┘
Go parallellikni aynan shu tarzda samarali boshqaradi.
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:
┌─────┐┌─────┐┌─────┐┌─────┐
│ 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.
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:
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())
}
/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:
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:
// CPU profile.
file, _ := os.Create("cpu.prof")
defer file.Close()
pprof.StartCPUProfile(file)
defer pprof.StopCPUProfile()
// ...
// 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:
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:
cfg := trace.FlightRecorderConfig{
MinAge: 5 * time.Second,
MaxBytes: 3 << 20, // 3MB
}
rec := trace.NewFlightRecorder(cfg)
rec.Start()
defer rec.Stop()
Biz Go tilida parallel dasturlash uchun mo'ljallangan quyidagi asosiy vosita va mexanizmlarni to'liq ko'rib chiqdik:
Ushbu vositalar Go tilini yuqori yuklamali, kengayuvchan va ishonchli tizimlar yaratishda dunyodagi eng ilg'or dasturlash tillaridan biriga aylantiradi.