Bir nechta goroutine bitta o'zgaruvchiga bir vaqtda murojaat qilsa va ulardan kamida bittasi ma'lumot yozsa, nima yuz beradi? Bu concurrent dasturlashning eng makkor va xavfli muammosi — **Data Race (Ma'lumotlar poygasi)**dir.
go run -race)Quyidagi oddiy hisoblagichni ko'rib chiqamiz:
package main
import (
"fmt"
"sync"
)
func main() {
counter := 0
var wg sync.WaitGroup
// 1000 ta goroutine counter ni bittadan oshiradi:
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++
}()
}
wg.Wait()
fmt.Println("Yakuniy natija:", counter)
}
Kutilgan natija: 1000.
Ammo ushbu kodni bir necha marta ishga tushirsangiz, har safar har xil kutilmagan javob chiqadi: 942, 987, 965...
Nima uchun 1000 chiqmadi?
Chunki counter++ amali bitta atomik amal emas! U mashina darajasida uchta bosqichdan iborat:
READ);INCREMENT);WRITE).Agar ikkita goroutine bir vaqtda xotiradan 10 sonini o'qib olsa, har ikkisi unga 1 qo'shib 11 qiladi va xotiraga ketma-ket yozadi. Natijada ikkita amal bajarilgan bo'lsa-da, hisoblagich faqat 1 taga oshadi!
Bunday noxush holatni bartaraf etish uchun Go standart kutubxonasida Mutex (Mutual Exclusion — o'zaro istisno) mavjud:
package main
import (
"fmt"
"sync"
)
func main() {
counter := 0
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock() // qulfni yopish (boshqa goroutine'lar navbatda kutadi)
counter++ // xavfsiz muhim hudud (critical section)
mu.Unlock() // qulfni ochish
}()
}
wg.Wait()
fmt.Println("Xavfsiz yakuniy natija:", counter) // Har doim aniq 1000!
}
mu.Lock() va mu.Unlock() oralig'iga muhim hudud (critical section) deyiladi. Bir vaqtning o'zida ushbu hududda faqat bitta goroutine bo'lishi qat'iy kafolatlanadi.
Agar sizda ma'lumotlar o'qilishi juda ko'p (99%), lekin yozilishi kam bo'lsa (1%), oddiy Mutex barcha o'quvchilarni ham navbatga turg'izib dasturni sekinlashtiradi.
Bunday holatlar uchun sync.RWMutex mavjud:
RLock() va RUnlock(): O'qish uchun qulf. Bir vaqtda yuzlab goroutine'lar parallel o'qishi mumkin!Lock() va Unlock(): Yozish uchun qulf. Faqat bitta goroutine yoza oladi, barcha o'quvchi va yozuvchilarni to'xtatib turadi.type SafeMap struct {
mu sync.RWMutex
data map[string]string
}
func (m *SafeMap) Get(key string) string {
m.mu.RLock() // parallel o'qish mumkin
defer m.mu.RUnlock()
return m.data[key]
}
func (m *SafeMap) Set(key, value string) {
m.mu.Lock() // faqat bitta yozuvchi
defer m.mu.Unlock()
m.data[key] = value
}
Dasturda data race bor-yo'qligini qanday aniqlash mumkin? Go kompilyatori o'z ichiga ajoyib vosita — Race Detectorni olgan! Shunchaki dasturni quyidagicha ishga tushiring:
go run -race main.go
go test -race ./...
Agar kodda ma'lumotlar poygasi bo'lsa, u qaysi goroutine va qaysi qatorda xotiraga noxolis murojaat qilganini aniq ko'rsatib beradi.
SafeCounter strukturasini tuzing va unga Inc() hamda Value() int metodlarini qo'shing. Dasturni poygasiz ishlaydigan holatga keltiring.
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
// TODO: mutex va count maydonlarini qo'shing
}
func (c *SafeCounter) Inc() {
// TODO: mutex bilan xavfsiz oshiring
}
func (c *SafeCounter) Value() int {
// TODO: xavfsiz qiymatni qaytaring
return 0
}
func main() {
c := SafeCounter{}
var wg sync.WaitGroup
for i := 0; i < 500; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Inc()
}()
}
wg.Wait()
fmt.Println("Natija:", c.Value())
}
Data race Go concurrent dasturlashda eng ko'p yo'l qo'yiladigan xatolardan biri bo'lib, sync.Mutex va sync.RWMutex uni bartaraf etishning eng mustahkam poydevoridir.
Keyingi mavzu: Race conditions (Mantiqiy poyga holatlari va Check-then-act)!