18-bo‘lim
Gorutinalar
go kalit so'zi, main tugashi muammosi, sync.WaitGroup bilan kutish, poyga holati, Mutex va -race bayrog'i.
Ushbu bo‘lim mundarijasi
Gorutina - bir vaqtda ishlaydigan mustaqil funksiya. Uni ishga
tushirish uchun bitta kalit so'z yetarli: go.
Bu Go ning eng mashhur imkoniyati va til aynan shu uchun yaratilgan.
Birinchi gorutina #
package main
import (
"fmt"
"time"
)
func salomlash(ism string) {
fmt.Println("Salom,", ism)
}
func main() {
go salomlash("Malika")
time.Sleep(100 * time.Millisecond)
fmt.Println("main tugadi")
}
Salom, Malika
main tugadi
go salomlash("Malika") - funksiyani alohida ishga tushiradi
va darhol keyingi qatorga o'tadi.
Yuqoridagi time.Sleep ni olib tashlang - "Salom" umuman
chiqmaydi.
Sabab: main tugaganda dastur to'xtaydi va boshqa gorutinalar
tugaganini kutmaydi. Gorutina ishga tushishga ham ulgurmaydi.
Lekin time.Sleep - yomon yechim. U qancha kutish kerakligini
taxmin qiladi: sekin kompyuterda yetmasligi, tezida esa vaqtni
bekorga sarflashi mumkin.
To'g'ri yo'l - sync.WaitGroup. U darhol quyida.
WaitGroup bilan kutish #
package main
import (
"fmt"
"sync"
)
func main() {
var kutuvchi sync.WaitGroup
natijalar := make([]int, 5)
for i := range 5 {
kutuvchi.Add(1)
go func() {
defer kutuvchi.Done()
natijalar[i] = i * i
}()
}
kutuvchi.Wait()
fmt.Println(natijalar)
fmt.Println("hammasi tugadi")
}
[0 1 4 9 16]
hammasi tugadi
sync.WaitGroup - oddiy hisoblagich:
| Metod | Nima qiladi |
|---|---|
Add(1) | Hisoblagichni oshiradi |
Done() | Kamaytiradi (odatda defer bilan) |
Wait() | Nol bo'lguncha kutadi |
Gorutinalar qaysi tartibda tugashini bilib bo'lmaydi. Agar har
biri fmt.Println qilsa, natija har safar boshqacha chiqardi.
Shuning uchun bu yerda har bir gorutina o'z indeksiga yozadi, chop etish esa hammasi tugagandan keyin, bitta joyda bajariladi.
Bu xavfsiz: har bir gorutina slicening boshqa katakchasiga yozmoqda, shuning uchun to'qnashuv yo'q.
Bu darslikdagi hamma parallel misol shu tamoyilga amal qiladi - aks holda kutilgan chiqishni yozib bo'lmasdi.
Gorutinalar arzon #
package main
import (
"fmt"
"sync"
)
func main() {
var kutuvchi sync.WaitGroup
var qulf sync.Mutex
jami := 0
for range 10000 {
kutuvchi.Add(1)
go func() {
defer kutuvchi.Done()
qulf.Lock()
jami++
qulf.Unlock()
}()
}
kutuvchi.Wait()
fmt.Println("gorutinalar soni:", jami)
}
gorutinalar soni: 10000
O'n ming gorutina - bu Go uchun oddiy hol. Har biri boshida atigi bir necha kilobayt oladi va kerak bo'lsa o'sadi.
Operatsion tizim oqimi (thread) odatda bir megabayt xotira oladi. O'n ming oqim - o'n gigabayt.
Go esa gorutinalarni o'z ichida boshqaradi: minglab gorutina bir nechta haqiqiy oqim ustida ishlaydi. Go ning ichki rejalashtiruvchisi ularni oqimlar orasida taqsimlaydi.
Gorutina biror narsani kutib qolsa - masalan, fayl yoki tarmoq - rejalashtiruvchi o'sha oqimga boshqa gorutinani qo'yadi. Shuning uchun kutish tekin.
Aynan shu sabab Go serverlar uchun juda mos: har bir so'rovga bitta gorutina ajratish mumkin.
Poyga holati #
package main
import (
"fmt"
"sync"
)
func main() {
var kutuvchi sync.WaitGroup
xavfli := 0
for range 1000 {
kutuvchi.Add(1)
go func() {
defer kutuvchi.Done()
xavfli++
}()
}
kutuvchi.Wait()
fmt.Println("1000 ga teng bo'lishi shart edi:", xavfli <= 1000)
fmt.Println("lekin kafolat yo'q")
}
1000 ga teng bo'lishi shart edi: true
lekin kafolat yo'q
xavfli++ sodda ko'rinadi, lekin protsessor uchun bu uch qadam:
qiymatni o'qish, bittaga oshirish, qaytib yozish.
Ikki gorutina bir vaqtda o'qisa, ikkalasi ham bir xil eski qiymatni ko'radi, ikkalasi ham bir xil yangi qiymatni yozadi - va bitta oshirish yo'qoladi.
Bu poyga holati (race condition). Uning eng yomon tomoni: natija ba'zan to'g'ri chiqadi. Kichik yuklamada dastur yillab ishlashi, keyin serverda kutilmaganda buzilishi mumkin.
Shuning uchun yuqoridagi misolda aniq son emas, xavfli <= 1000
tekshiruvi chiqarildi - aniq qiymat kafolatlanmaydi.
-race bayrog'i #
Go da poygalarni topadigan tayyor vosita bor:
go run -race main.go
U dasturni kuzatib turadi va to'qnashuv topilsa, shunday ogohlantiradi:
WARNING: DATA RACE
Read at 0x00c000132000 by goroutine 8:
main.main.func1()
/tmp/misol/main.go:16 +0x3c
Previous write at 0x00c000132000 by goroutine 7:
main.main.func1()
/tmp/misol/main.go:16 +0x50
-race dasturni bir necha barobar sekinlashtiradi, shuning uchun
uni ishlab chiqarishda ishlatmaydi.
Lekin testlarda u majburiy bo'lishi kerak:
go test -race ./...
U faqat haqiqatan sodir bo'lgan to'qnashuvni topadi. Shuning uchun testlar parallel kodni yaxshi qamrab olishi muhim.
Bu vosita Go ning eng qimmatli qismlaridan biri - ko'p tillarda bunday narsa umuman yo'q.
Mutex bilan himoya #
package main
import (
"fmt"
"sync"
)
type Hisoblagich struct {
qulf sync.Mutex
soni map[string]int
}
func YangiHisoblagich() *Hisoblagich {
return &Hisoblagich{soni: make(map[string]int)}
}
func (h *Hisoblagich) Oshir(kalit string) {
h.qulf.Lock()
defer h.qulf.Unlock()
h.soni[kalit]++
}
func (h *Hisoblagich) Qiymat(kalit string) int {
h.qulf.Lock()
defer h.qulf.Unlock()
return h.soni[kalit]
}
func main() {
h := YangiHisoblagich()
var kutuvchi sync.WaitGroup
for range 500 {
kutuvchi.Add(1)
go func() {
defer kutuvchi.Done()
h.Oshir("sorov")
}()
}
kutuvchi.Wait()
fmt.Println(h.Qiymat("sorov"))
}
500
Mutex - "bir vaqtda faqat bitta gorutina kirsin" degan qulf.
Lock() va Unlock() orasidagi kod himoyalangan bo'ladi.
defer bilan yozish muhim: funksiya qanday tugashidan qat'i
nazar, qulf ochiladi.
E'tibor bering: qulf maydoni himoyalayotgan soni maydoni bilan
bitta strukturada turadi.
Bu ataylab: qulf va u himoyalaydigan ma'lumot doim yonma-yon bo'lishi kerak. Aks holda kimdir ma'lumotni qulfsiz ishlatib yuborishi mumkin.
Va yana: Hisoblagich metodlari ko'rsatkich resiverli -
12-bo'limdagi qoida. sync.Mutex ni nusxalab bo'lmaydi, nusxa
boshqa qulf bo'lib qoladi. go vet bu xatoni ham topadi.
Faqat o'qish uchun RWMutex #
package main
import (
"fmt"
"sync"
)
type Ombor struct {
qulf sync.RWMutex
narxlar map[string]int
}
func (o *Ombor) Yoz(nom string, narx int) {
o.qulf.Lock()
defer o.qulf.Unlock()
o.narxlar[nom] = narx
}
func (o *Ombor) Oqi(nom string) int {
o.qulf.RLock()
defer o.qulf.RUnlock()
return o.narxlar[nom]
}
func main() {
o := &Ombor{narxlar: make(map[string]int)}
o.Yoz("olma", 12000)
var kutuvchi sync.WaitGroup
natijalar := make([]int, 100)
for i := range 100 {
kutuvchi.Add(1)
go func() {
defer kutuvchi.Done()
natijalar[i] = o.Oqi("olma")
}()
}
kutuvchi.Wait()
hammasiTogri := true
for _, n := range natijalar {
if n != 12000 {
hammasiTogri = false
}
}
fmt.Println(hammasiTogri)
}
true
sync.RWMutex ikki xil qulf beradi:
| Metod | Ma'nosi |
|---|---|
Lock / Unlock | Yozish - bittagina gorutina |
RLock / RUnlock | O'qish - bir nechta birga |
O'qish yozishdan ancha ko'p bo'lgan holatlarda bu sezilarli tezlik beradi.
Sikl o'zgaruvchisi - endi xavfsiz #
package main
import (
"fmt"
"slices"
"sync"
)
func main() {
var kutuvchi sync.WaitGroup
var qulf sync.Mutex
yiqilgan := []int{}
for i := range 5 {
kutuvchi.Add(1)
go func() {
defer kutuvchi.Done()
qulf.Lock()
yiqilgan = append(yiqilgan, i)
qulf.Unlock()
}()
}
kutuvchi.Wait()
slices.Sort(yiqilgan)
fmt.Println(yiqilgan)
}
[0 1 2 3 4]
7-bo'limda ko'rgan o'zgarish aynan shu yerda eng ko'p bilinardi.
Go 1.21 va undan oldin hamma gorutina bitta i ga
bog'lanardi. Natijada ko'pincha [5 5 5 5 5] chiqardi.
Buni tuzatish uchun i := i deb nusxa olish yoki go func(n int)
deb argument sifatida uzatish kerak edi.
Go 1.22 dan beri har bir aylanish o'z i sini oladi va bu
xato butunlay yo'qoldi. Eski kodni o'qiyotganda shu farqni yodda
tuting.
E'tibor bering: slices.Sort baribir kerak - gorutinalar
tartibi hali ham kafolatlanmaydi.
Gorutina oqib ketishi #
Gorutina o'z-o'zidan to'xtamaydi. Agar u biror narsani abadiy kutib qolsa, dastur tugaguncha xotirada qoladi:
func oqadi() {
kanal := make(chan int)
go func() {
qiymat := <-kanal // hech kim yubormaydi
fmt.Println(qiymat)
}()
// funksiya tugadi, gorutina esa abadiy kutmoqda
}
Bu gorutina oqishi (goroutine leak). Serverda har bir so'rov bitta gorutina qoldirsa, xotira asta-sekin to'lib boradi.
Qoida: har bir gorutina uchun "u qanday tugaydi?" degan savolga
javobingiz bo'lsin. Keyingi bo'limdagi context va kanalni yopish
shu savolga javob beradi.
gobilan funksiya ishga tushiring va hech narsa chiqmasligini ko'ring.WaitGroupqo'shib, natijani oling.AddvaDonesonini teng qilmay ko'ring.- Ming ta gorutina ishga tushiring.
- Qulfsiz hisoblagich yozib,
-racebilan ishga tushiring. sync.Mutexbilan uni to'g'rilang.- Qulf va ma'lumotni bitta strukturaga joylang.
RWMutexbilan o'qishni parallel qiling.- Natijalarni indeks bo'yicha slicega yig'ing.
- Gorutinalar tartibi har safar o'zgarishini kuzating.
Xulosa #
go funksiya()- gorutinani ishga tushiradi va darhol davom etadi.maintugasa, hamma gorutina o'ladi;time.Sleepbilan kutish - yomon yechim.sync.WaitGroup-Add,Done,Waituchligi bilan to'g'ri kutish usuli.- Gorutina arzon: bir necha kilobayt, o'n minglabi oddiy hol.
xavfli++atomar emas - bu poyga holatiga olib keladi.go test -racepoygalarni topadi; uni testlarda doim ishlating.sync.Mutexkodni himoyalaydi; qulfni u himoyalaydigan ma'lumot bilan bitta strukturada saqlang.sync.RWMutexbir nechta o'quvchiga birga ruxsat beradi.- Go 1.22 dan beri sikl o'zgaruvchisi har aylanishda yangi - eski klassik xato yo'qoldi.
- Har bir gorutina uchun "u qanday tugaydi?" degan javobingiz bo'lsin.
Keyingi bo'limda kanallarni o'rganamiz.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
Xatolik topdingizmi?
Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.