Go / WaitGroup ба Mutex

WaitGroup ба Mutex

Горутинүүд нэгэн зэрэг ажиллахад хоёр нийтлэг асуудал гардаг: бүх горутин дуустал хэрхэн хүлээх, хуваалцсан өгөгдлийг хэрхэн аюулгүй өөрчлөх. sync package-ийн WaitGroup ба Mutex яг эдгээр асуудлыг шийддэг.

sync.WaitGroup — горутин хүлээх

time.Sleep ашиглан горутин хүлээх нь найдваргүй. WaitGroup нь тохирох хэмжээний хугацааг хүлээдэг:

go
package main

import (
    "fmt"
    "sync"
)

func download(url string, wg *sync.WaitGroup) {
    defer wg.Done() // функц дуусахад -1 хийнэ
    fmt.Printf("Татаж байна: %s\n", url)
}

func main() {
    var wg sync.WaitGroup

    urls := []string{
        "https://example.com/file1",
        "https://example.com/file2",
        "https://example.com/file3",
    }

    for _, url := range urls {
        wg.Add(1) // тоолуур +1
        go download(url, &wg)
    }

    wg.Wait() // бүх горутин дуустал хүлээ
    fmt.Println("Бүх таталт дууслаа")
}

wg.Add(1) — горутин эхлэхийн өмнө тоолуурыг нэмнэ. wg.Done() — горутин дуусахад тоолуурыг хасна (defer ашигласан нь зөв хэв маяг). wg.Wait() — тоолуур тэг болтол хаагдана.

Race Condition — аюулт нөхцөл байдал

Олон горутин нэгэн зэрэг нэг хувьсагчийг өөрчилвөл race condition үүснэ:

go
package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    count := 0 // хуваалцсан хувьсагч

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            count++ // АЮУЛТ! Нэгэн зэрэг өөрчилж байна
        }()
    }

    wg.Wait()
    fmt.Println("Тоо (буруу байж болно):", count)
    // Хүлээгдэж буй: 1000, бодит: тодорхойгүй
}

count++ нь дотроо гурван үйлдэл: унших, нэмэх, бичих. Горутинүүд эдгээр үйлдлийн хооронд огтлолцож болно.

sync.Mutex — хамгаалагч түгжээ

Mutex (Mutual Exclusion) нь нэгэн зэрэг зөвхөн нэг горутинд нэвтрэх боломж олгодог:

go
package main

import (
    "fmt"
    "sync"
)

type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()   // түгжих — бусад горутин энд хүлээнэ
    c.count++
    c.mu.Unlock() // тайлах — бусад горутин үргэлжилнэ
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

func main() {
    var wg sync.WaitGroup
    counter := &SafeCounter{}

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment()
        }()
    }

    wg.Wait()
    fmt.Println("Тоо (зөв):", counter.Value()) // Үргэлж 1000
}

mu.Lock() ба mu.Unlock() хоёрын хооронд нэгэн зэрэг зөвхөн нэг горутин байж болно.

sync.RWMutex — уншилт/бичилтийн түгжээ

Уншилт олон горутинд нэгэн зэрэг зөвшөөрч, бичилтийг хязгаарлах:

go
package main

import (
    "fmt"
    "sync"
)

type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Set(key, value string) {
    c.mu.Lock() // бичилтэд бүтэн түгжээ
    defer c.mu.Unlock()
    c.data[key] = value
}

func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock() // уншилтад зөвхөн уншилтын түгжээ
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

func main() {
    cache := &Cache{data: make(map[string]string)}
    var wg sync.WaitGroup

    // Нэгэн зэрэг бичих
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            cache.Set(fmt.Sprintf("түлхүүр%d", n), fmt.Sprintf("утга%d", n))
        }(i)
    }

    wg.Wait()

    // Нэгэн зэрэг унших
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            val, _ := cache.Get(fmt.Sprintf("түлхүүр%d", n))
            fmt.Printf("түлхүүр%d = %s\n", n, val)
        }(i)
    }

    wg.Wait()
}

RWMutex нь уншилт их, бичилт цөөн тохиолдолд Mutex-ээс хурдан ажилладаг.

Дараагийн хичээлд:

Go кодыг package болгон зохион байгуулах үндсийг сурна. Package нь кодыг дахин ашиглах, тусгаарлах гол механизм юм.