sync paketi
Goroutine'lar haqidagi darsda bir nechta jarayonni bir vaqtda ishga tushirishni ko'rgan edik. Lekin bir nechta goroutine bitta umumiy o'zgaruvchiga bir vaqtda murojaat qilsa nima bo'ladi? Ana shu — "poyga sharoiti" (race condition) muammosini xavfsiz hal qilish uchun sync paketi ishlatiladi.
import "sync"
package main
import (
"fmt"
"sync"
)
func main() {
var counter int
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ // BIR NECHTA goroutine buni bir vaqtda o'zgartirmoqchi bo'ladi!
}()
}
wg.Wait()
fmt.Println(counter)
}
Bu kodni ishga tushirsangiz, natija ko'pincha 1000 dan kichik chiqadi (masalan, 978)! Sababi: counter++ aslida uchta amaldan iborat (o'qish → 1 qo'shish → yozish). Ikkita goroutine bir vaqtda bir xil eski qiymatni o'qib olib, ikkalasi ham unga alohida-alohida +1 qilib qaytarib yozsa, natijada bitta qo'shish "yo'qolib" ketadi.
Bunday muammoni poyga sharoiti (race condition) deb ataymiz, va uni sync.Mutex yordamida hal qilamiz.
sync.MutexMutex (mutual exclusion — "o'zaro chetlab o'tish") bir vaqtning o'zida faqat bitta goroutine kodning himoyalangan qismiga kirishini ta'minlaydi:
package main
import (
"fmt"
"sync"
)
func main() {
var counter int
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock() // navbatga turadi (agar boshqa goroutine qulflagan bo'lsa, kutadi)
counter++ // faqat bitta goroutine bu yerga bir vaqtning o'zida kira oladi
mu.Unlock() // qulfni bo'shatadi, navbatdagi goroutine kira oladi
}()
}
wg.Wait()
fmt.Println(counter)
}
Natija (har doim aynan shunday, endi kafolatlangan):
1000
[!IMPORTANT]
mu.Lock()vamu.Unlock()doim juft chaqirilishi shart. AgarUnlock()chaqirilmasdan qolib ketsa (masalan, oraliqda funksiya ertareturnqilib ketsa), qolgan barcha goroutine'lar abadiy kutib qoladi ("deadlock"). Shu sabablimu.Unlock()ni ko'pinchadefer mu.Unlock()orqaliLock()dan darhol keyin yozish tavsiya etiladi.
sync.WaitGroupYuqoridagi misollarda allaqachon ko'rgan WaitGroup — main() funksiyasi barcha goroutine'lar tugaguncha kutib turishini ta'minlaydi:
var wg sync.WaitGroup
wg.Add(1) // "yana bitta goroutine kutilyapti" deb belgilaydi
go func() {
defer wg.Done() // goroutine tugaganda "bitta kam qoldi" deydi
// ish...
}()
wg.Wait() // barcha Add() lar Done() bilan tenglashguncha shu yerda kutadi
[!WARNING]
wg.Add()sonini goroutine ichida emas, goroutine boshlanishidan oldin chaqiring. Aks holdaWait()goroutine haliAdd()chaqirmasdan turib "hammasi tugadi" deb noto'g'ri xulosaga kelishi mumkin.
sync.OnceBa'zan (masalan, konfiguratsiyani yuklash yoki bog'lanishni ishga tushirish) qandaydir amal — dastur necha marta chaqirilishidan qat'i nazar — faqat bir marta bajarilishi kerak bo'ladi:
package main
import (
"fmt"
"sync"
)
func main() {
var once sync.Once
for i := 0; i < 3; i++ {
once.Do(func() {
fmt.Println("Faqat bir marta ishga tushadi")
})
}
}
Natija:
Faqat bir marta ishga tushadi
Sikl 3 marta aylansa ham, once.Do() ichidagi funksiya faqat birinchi chaqiruvda ishlaydi — qolganlarida Once buni allaqachon bajarilganini "eslab qoladi" va e'tiborsiz qoldiradi.
sync.RWMutexAgar umumiy ma'lumot ko'p o'qiladigan, lekin kamdan-kam yoziladigan bo'lsa, oddiy Mutex o'rniga RWMutex (Read-Write Mutex) samaraliroq: u bir vaqtning o'zida bir nechta o'quvchiga ruxsat beradi, faqat yozish paytida to'liq qulflaydi:
package main
import (
"fmt"
"sync"
)
func main() {
var rw sync.RWMutex
// bir nechta goroutine bir vaqtda RLock() qila oladi (o'qish uchun)
rw.RLock()
fmt.Println("O'qish qulfi ochildi")
rw.RUnlock()
// Lock() esa hech kim (na o'quvchi, na yozuvchi) kirmasin deydi
rw.Lock()
fmt.Println("Yozish qulfi ochildi")
rw.Unlock()
}
Natija:
O'qish qulfi ochildi
Yozish qulfi ochildi
sync.MapOddiy Go mapi bir nechta goroutine tomonidan bir vaqtda o'zgartirilsa, dastur "panik" qiladi. Agar bunday xavfsizlik kerak bo'lsa, sync.Mutex bilan o'ralgan oddiy map o'rniga tayyor **sync.Map**dan foydalanish mumkin:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
m.Store("key1", "value1") // yozish
val, ok := m.Load("key1") // o'qish
fmt.Println(val, ok)
}
Natija:
value1 true
[!NOTE]
sync.Mapfaqat maxsus holatlar uchun (masalan, kalitlar to'plami kam o'zgaradigan, lekin ko'p goroutine tomonidan o'qiladigan holatlarda) optimallashtirilgan. Odatiy holatlarda oddiymap+sync.Mutexkombinatsiyasi ko'pincha yetarli va tushunarliroq.
| Vazifa | Vosita |
|---|---|
| Umumiy o'zgaruvchini bir vaqtda o'zgartirishdan himoya qilish | sync.Mutex |
| Ko'p o'qish, kam yozish holatlarida samarali qulf | sync.RWMutex |
| Bir nechta goroutine tugashini kutish | sync.WaitGroup |
| Amalni faqat bir marta bajarish | sync.Once |
| Konkurentlikka xavfsiz kalit-qiymat saqlash | sync.Map |
[!IMPORTANT] Agar goroutine'lar orasida ma'lumot almashish kerak bo'lsa, ko'pincha
channel(oldingi darsda ko'rgan edik)sync.Mutexdan tabiiyroq yechim bo'ladi. Go jamiyatida mashhur qoida bor: "Xotirani ulashish orqali aloqa qilmang, aloqa qilish orqali xotirani ulashing." Ya'ni imkon qadarchannelishlatishga harakat qiling,syncpaketini esa aynan umumiy holatni (masalan, hisoblagich) himoya qilish zarur bo'lgandagina ishlating.