65 / 68

sync

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.

example.go
import "sync"

1. Muammo: bir nechta goroutine bitta o'zgaruvchini o'zgartirsa

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


2. Umumiy resursni himoyalash: sync.Mutex

Mutex (mutual exclusion — "o'zaro chetlab o'tish") bir vaqtning o'zida faqat bitta goroutine kodning himoyalangan qismiga kirishini ta'minlaydi:

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 (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):

kod
1000

[!IMPORTANT] mu.Lock() va mu.Unlock() doim juft chaqirilishi shart. Agar Unlock() chaqirilmasdan qolib ketsa (masalan, oraliqda funksiya erta return qilib ketsa), qolgan barcha goroutine'lar abadiy kutib qoladi ("deadlock"). Shu sababli mu.Unlock()ni ko'pincha defer mu.Unlock() orqali Lock()dan darhol keyin yozish tavsiya etiladi.


3. Bir nechta goroutine tugashini kutish: sync.WaitGroup

Yuqoridagi misollarda allaqachon ko'rgan WaitGroup — main() funksiyasi barcha goroutine'lar tugaguncha kutib turishini ta'minlaydi:

example.go
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 holda Wait() goroutine hali Add() chaqirmasdan turib "hammasi tugadi" deb noto'g'ri xulosaga kelishi mumkin.


4. Amalni faqat bir marta bajarish: sync.Once

Ba'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:

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

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


5. O'qish uchun optimallashtirilgan qulf: sync.RWMutex

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

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

kod
O'qish qulfi ochildi
Yozish qulfi ochildi

6. Konkurentlikka xavfsiz map: sync.Map

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

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

kod
value1 true

[!NOTE] sync.Map faqat maxsus holatlar uchun (masalan, kalitlar to'plami kam o'zgaradigan, lekin ko'p goroutine tomonidan o'qiladigan holatlarda) optimallashtirilgan. Odatiy holatlarda oddiy map + sync.Mutex kombinatsiyasi ko'pincha yetarli va tushunarliroq.


Xulosa: qachon qaysi vosita kerak?

VazifaVosita
Umumiy o'zgaruvchini bir vaqtda o'zgartirishdan himoya qilishsync.Mutex
Ko'p o'qish, kam yozish holatlarida samarali qulfsync.RWMutex
Bir nechta goroutine tugashini kutishsync.WaitGroup
Amalni faqat bir marta bajarishsync.Once
Konkurentlikka xavfsiz kalit-qiymat saqlashsync.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 qadar channel ishlatishga harakat qiling, sync paketini esa aynan umumiy holatni (masalan, hisoblagich) himoya qilish zarur bo'lgandagina ishlating.