Muammo: bir nechta goroutine bitta o'zgaruvchini o'zgartirsa
Oldingi darsda har bir goroutine o'ziga tegishli xotira katakchasiga yozganini ko'rdik — bu xavfsiz edi. Endi bir nechta goroutine bitta umumiy o'zgaruvchini bir vaqtda o'zgartirsa nima bo'lishini ko'ramiz:
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 bir necha marta ishga tushirib ko'rsangiz, natija har safar boshqacha va deyarli har doim 1000 dan kichik chiqadi (masalan, 967, keyingi safar 941...). Bu — poyga holati (race condition) deb ataladi.
[!IMPORTANT] Sababi:
counter++aslida bitta emas, uchta amaldan iborat: (1)counterning joriy qiymatini o'qish, (2) unga1qo'shish, (3) natijani qaytarib yozish. Agar ikkita goroutine bu uch amalni bir vaqtga to'g'ri kelib bajarsa, ikkalasi ham bir xil "eski" qiymatni o'qib olib, alohida-alohida+1qilib qaytarib yozadi — natijada bitta qo'shish "yo'qolib" ketadi.
go run -raceGoda race condition'larni "ko'z bilan" topish qiyin (chunki dastur ko'pincha "to'g'ri" ishlagandek tuyuladi), shuning uchun Go maxsus poyga aniqlagich (race detector) bilan keladi:
go run -race main.go
Agar yuqoridagi xavfli kod ustida shu buyruq ishga tushirilsa, Go quyidagiga o'xshash ogohlantirish chiqaradi:
WARNING: DATA RACE
Read at 0x00c00019e048 by goroutine 15:
main.main.func1()
Previous write at 0x00c00019e048 by goroutine 14:
main.main.func1()
[!NOTE]
-racebayrog'i dasturni sekinroq ishlatadi (2-20 barobar), shuning uchun uni faqat ishlab chiqish va test qilish vaqtida ishlatish tavsiya etiladi, ishlab chiqarish (production) muhitida emas. Lekin CI/CD jarayonida testlarni doimgo test -racebilan ham ishga tushirish juda foydali odat hisoblanadi.
sync.MutexMutex (mutual exclusion — "o'zaro chetlab o'tish") umumiy resursga bir vaqtning o'zida faqat bitta goroutine kirishini kafolatlaydi:
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
defer mu.Unlock() // funksiya tugaganda avtomatik bo'shatiladi
counter++ // endi faqat bitta goroutine bu yerga bir vaqtda kira oladi
}()
}
wg.Wait()
fmt.Println(counter)
}
Natija (endi har doim, kafolatlangan):
1000
mu.Lock() chaqirilgan zahoti, boshqa har qanday goroutine xuddi shu muni qulflashga urinib, avvalgi goroutine mu.Unlock() chaqirmaguncha kutib turadi. Shu tarzda, counter++ amali har doim bittalab, ketma-ket bajariladi — hech qachon ikkita goroutine bir vaqtda emas.
Lock()/Unlock() bilan ishlashning oltin qoidasimu.Lock()
defer mu.Unlock() // Lock()dan DARHOL keyin yozing
[!WARNING]
Lock()vaUnlock()doim juft chaqirilishi shart. AgarUnlock()chaqirilmasdan qolib ketsa (masalan, orasida funksiya ertareturnqilib yuborsa yokipanicyuz bersa), qolgan barcha goroutinelar o'sha qulf ustida abadiy kutib qoladi — bu holat deadlock (o'lik qulflanish) deyiladi. Shuning uchunmu.Unlock()ni deyarli har doimmu.Lock()dan darhol keyin,deferorqali yozish eng xavfsiz odat hisoblanadi.
O'zini-o'zi qulflashning eng oddiy (va eng ko'p uchraydigan) namunasi:
var mu sync.Mutex
mu.Lock()
fmt.Println("Qulflandi")
mu.Lock() // XATO! Bu goroutine allaqachon qulflagan, endi o'zini-o'zi abadiy kutadi
fmt.Println("Bu yerga hech qachon yetib kelmaydi")
Bu kodni ishga tushirsangiz, dastur "osilib qoladi" va hech qachon tugamaydi.
sync.RWMutexAgar umumiy ma'lumot ko'p o'qiladigan, lekin kamdan-kam yoziladigan bo'lsa (masalan, konfiguratsiya sozlamalari), oddiy Mutex samarasiz bo'lishi mumkin — chunki u hatto ikkita "faqat o'quvchi" goroutine'ni ham bir vaqtda kiritmaydi. Bunday holatlar uchun sync.RWMutex mo'ljallangan:
package main
import (
"fmt"
"sync"
)
type SafeConfig struct {
mu sync.RWMutex
data map[string]string
}
func (c *SafeConfig) Get(key string) string {
c.mu.RLock() // bir nechta goroutine bir vaqtda RLock() qila oladi
defer c.mu.RUnlock()
return c.data[key]
}
func (c *SafeConfig) Set(key, value string) {
c.mu.Lock() // Lock() esa hech kimni (na o'quvchi, na yozuvchi) kiritmaydi
defer c.mu.Unlock()
c.data[key] = value
}
func main() {
config := &SafeConfig{data: make(map[string]string)}
config.Set("til", "Go")
fmt.Println(config.Get("til"))
}
Natija:
Go
Mutex | RWMutex | |
|---|---|---|
| O'qish uchun | Bitta goroutine kirishi mumkin | Bir nechta goroutine bir vaqtda o'qiy oladi (RLock) |
| Yozish uchun | Bitta goroutine kirishi mumkin | Faqat bitta goroutine, hech kim (na o'quvchi) kirmaydi (Lock) |
| Qachon ishlatiladi | Oddiy holatlarda, yoki yozish tez-tez bo'lsa | O'qish yozishdan ancha ko'p bo'lgan holatlarda |
| Vazifa | Vosita |
|---|---|
| Umumiy o'zgaruvchini himoya qilish | sync.Mutex |
| Ko'p o'qish, kam yozish holatida samarali himoya | sync.RWMutex |
| Race condition'larni aniqlash | go run -race / go test -race |
[!NOTE] Yodda tuting: agar goroutinelar orasida umumiy o'zgaruvchi bo'lmasa (masalan, oldingi darsdagi kabi har biri o'z indeksiga yozsa),
Mutexham kerak emas.Mutexni faqat haqiqatan ham bir nechta goroutine bir xil xotira manziliga (o'zgaruvchi, map, slays) bir vaqtda murojaat qilishi mumkin bo'lgandagina ishlating — ortiqcha qulflash dasturni sekinlashtiradi va kodni murakkablashtiradi.