39 / 68

Poyga holati va sync.Mutex

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:

example.go
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) unga 1 qo'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 +1 qilib qaytarib yozadi — natijada bitta qo'shish "yo'qolib" ketadi.


1. Muammoni aniqlash: go run -race

Goda 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:

bash
go run -race main.go

Agar yuqoridagi xavfli kod ustida shu buyruq ishga tushirilsa, Go quyidagiga o'xshash ogohlantirish chiqaradi:

kod
WARNING: DATA RACE
Read at 0x00c00019e048 by goroutine 15:
  main.main.func1()
Previous write at 0x00c00019e048 by goroutine 14:
  main.main.func1()

[!NOTE] -race bayrog'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 doim go test -race bilan ham ishga tushirish juda foydali odat hisoblanadi.


2. Yechim: sync.Mutex

Mutex (mutual exclusion — "o'zaro chetlab o'tish") umumiy resursga bir vaqtning o'zida faqat bitta goroutine kirishini kafolatlaydi:

example.go
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):

kod
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.


3. Lock()/Unlock() bilan ishlashning oltin qoidasi

example.go
mu.Lock()
defer mu.Unlock() // Lock()dan DARHOL keyin yozing

[!WARNING] Lock() va Unlock() doim juft chaqirilishi shart. Agar Unlock() chaqirilmasdan qolib ketsa (masalan, orasida funksiya erta return qilib yuborsa yoki panic yuz bersa), qolgan barcha goroutinelar o'sha qulf ustida abadiy kutib qoladi — bu holat deadlock (o'lik qulflanish) deyiladi. Shuning uchun mu.Unlock()ni deyarli har doim mu.Lock()dan darhol keyin, defer orqali yozish eng xavfsiz odat hisoblanadi.

O'zini-o'zi qulflashning eng oddiy (va eng ko'p uchraydigan) namunasi:

example.go
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.


4. Ko'p o'qish, kam yozish: sync.RWMutex

Agar 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:

example.go
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:

kod
Go
MutexRWMutex
O'qish uchunBitta goroutine kirishi mumkinBir nechta goroutine bir vaqtda o'qiy oladi (RLock)
Yozish uchunBitta goroutine kirishi mumkinFaqat bitta goroutine, hech kim (na o'quvchi) kirmaydi (Lock)
Qachon ishlatiladiOddiy holatlarda, yoki yozish tez-tez bo'lsaO'qish yozishdan ancha ko'p bo'lgan holatlarda

Xulosa: qachon qaysi vosita kerak?

VazifaVosita
Umumiy o'zgaruvchini himoya qilishsync.Mutex
Ko'p o'qish, kam yozish holatida samarali himoyasync.RWMutex
Race condition'larni aniqlashgo 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), Mutex ham 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.