18-bo‘lim

Gorutinalar

go kalit so'zi, main tugashi muammosi, sync.WaitGroup bilan kutish, poyga holati, Mutex va -race bayrog'i.

🕑 14 daqiqa o‘qish 📄 961 so‘z 👁 0 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Birinchi gorutina
  2. WaitGroup bilan kutish
  3. Gorutinalar arzon
  4. Poyga holati
  5. -race bayrog'i
  6. Mutex bilan himoya
  7. Faqat o'qish uchun RWMutex
  8. Sikl o'zgaruvchisi - endi xavfsiz
  9. Gorutina oqib ketishi
  10. Xulosa

Gorutina - bir vaqtda ishlaydigan mustaqil funksiya. Uni ishga tushirish uchun bitta kalit so'z yetarli: go.

Bu Go ning eng mashhur imkoniyati va til aynan shu uchun yaratilgan.

Birinchi gorutina #

Go
package main

import (
    "fmt"
    "time"
)

func salomlash(ism string) {
    fmt.Println("Salom,", ism)
}

func main() {
    go salomlash("Malika")

    time.Sleep(100 * time.Millisecond)
    fmt.Println("main tugadi")
}
Natija
Salom, Malika
main tugadi

go salomlash("Malika") - funksiyani alohida ishga tushiradi va darhol keyingi qatorga o'tadi.

main tugasa, hamma gorutina o'ladi

Yuqoridagi time.Sleep ni olib tashlang - "Salom" umuman chiqmaydi.

Sabab: main tugaganda dastur to'xtaydi va boshqa gorutinalar tugaganini kutmaydi. Gorutina ishga tushishga ham ulgurmaydi.

Lekin time.Sleep - yomon yechim. U qancha kutish kerakligini taxmin qiladi: sekin kompyuterda yetmasligi, tezida esa vaqtni bekorga sarflashi mumkin.

To'g'ri yo'l - sync.WaitGroup. U darhol quyida.

WaitGroup bilan kutish #

Go
package main

import (
    "fmt"
    "sync"
)

func main() {
    var kutuvchi sync.WaitGroup

    natijalar := make([]int, 5)

    for i := range 5 {
        kutuvchi.Add(1)
        go func() {
            defer kutuvchi.Done()
            natijalar[i] = i * i
        }()
    }

    kutuvchi.Wait()
    fmt.Println(natijalar)
    fmt.Println("hammasi tugadi")
}
Natija
[0 1 4 9 16]
hammasi tugadi

sync.WaitGroup - oddiy hisoblagich:

MetodNima qiladi
Add(1)Hisoblagichni oshiradi
Done()Kamaytiradi (odatda defer bilan)
Wait()Nol bo'lguncha kutadi
Natijani nega slicega yozdik

Gorutinalar qaysi tartibda tugashini bilib bo'lmaydi. Agar har biri fmt.Println qilsa, natija har safar boshqacha chiqardi.

Shuning uchun bu yerda har bir gorutina o'z indeksiga yozadi, chop etish esa hammasi tugagandan keyin, bitta joyda bajariladi.

Bu xavfsiz: har bir gorutina slicening boshqa katakchasiga yozmoqda, shuning uchun to'qnashuv yo'q.

Bu darslikdagi hamma parallel misol shu tamoyilga amal qiladi - aks holda kutilgan chiqishni yozib bo'lmasdi.

Gorutinalar arzon #

Go
package main

import (
    "fmt"
    "sync"
)

func main() {
    var kutuvchi sync.WaitGroup
    var qulf sync.Mutex
    jami := 0

    for range 10000 {
        kutuvchi.Add(1)
        go func() {
            defer kutuvchi.Done()
            qulf.Lock()
            jami++
            qulf.Unlock()
        }()
    }

    kutuvchi.Wait()
    fmt.Println("gorutinalar soni:", jami)
}
Natija
gorutinalar soni: 10000

O'n ming gorutina - bu Go uchun oddiy hol. Har biri boshida atigi bir necha kilobayt oladi va kerak bo'lsa o'sadi.

Gorutina - bu oqim emas

Operatsion tizim oqimi (thread) odatda bir megabayt xotira oladi. O'n ming oqim - o'n gigabayt.

Go esa gorutinalarni o'z ichida boshqaradi: minglab gorutina bir nechta haqiqiy oqim ustida ishlaydi. Go ning ichki rejalashtiruvchisi ularni oqimlar orasida taqsimlaydi.

Gorutina biror narsani kutib qolsa - masalan, fayl yoki tarmoq - rejalashtiruvchi o'sha oqimga boshqa gorutinani qo'yadi. Shuning uchun kutish tekin.

Aynan shu sabab Go serverlar uchun juda mos: har bir so'rovga bitta gorutina ajratish mumkin.

Poyga holati #

Go
package main

import (
    "fmt"
    "sync"
)

func main() {
    var kutuvchi sync.WaitGroup
    xavfli := 0

    for range 1000 {
        kutuvchi.Add(1)
        go func() {
            defer kutuvchi.Done()
            xavfli++
        }()
    }
    kutuvchi.Wait()

    fmt.Println("1000 ga teng bo'lishi shart edi:", xavfli <= 1000)
    fmt.Println("lekin kafolat yo'q")
}
Natija
1000 ga teng bo'lishi shart edi: true
lekin kafolat yo'q
xavfli++ bitta amal emas

xavfli++ sodda ko'rinadi, lekin protsessor uchun bu uch qadam: qiymatni o'qish, bittaga oshirish, qaytib yozish.

Ikki gorutina bir vaqtda o'qisa, ikkalasi ham bir xil eski qiymatni ko'radi, ikkalasi ham bir xil yangi qiymatni yozadi - va bitta oshirish yo'qoladi.

Bu poyga holati (race condition). Uning eng yomon tomoni: natija ba'zan to'g'ri chiqadi. Kichik yuklamada dastur yillab ishlashi, keyin serverda kutilmaganda buzilishi mumkin.

Shuning uchun yuqoridagi misolda aniq son emas, xavfli <= 1000 tekshiruvi chiqarildi - aniq qiymat kafolatlanmaydi.

-race bayrog'i #

Go da poygalarni topadigan tayyor vosita bor:

Terminal
go run -race main.go

U dasturni kuzatib turadi va to'qnashuv topilsa, shunday ogohlantiradi:

Natija
WARNING: DATA RACE
Read at 0x00c000132000 by goroutine 8:
  main.main.func1()
      /tmp/misol/main.go:16 +0x3c

Previous write at 0x00c000132000 by goroutine 7:
  main.main.func1()
      /tmp/misol/main.go:16 +0x50
-race ni testlarda doim ishlating

-race dasturni bir necha barobar sekinlashtiradi, shuning uchun uni ishlab chiqarishda ishlatmaydi.

Lekin testlarda u majburiy bo'lishi kerak:

Terminal
go test -race ./...

U faqat haqiqatan sodir bo'lgan to'qnashuvni topadi. Shuning uchun testlar parallel kodni yaxshi qamrab olishi muhim.

Bu vosita Go ning eng qimmatli qismlaridan biri - ko'p tillarda bunday narsa umuman yo'q.

Mutex bilan himoya #

Go
package main

import (
    "fmt"
    "sync"
)

type Hisoblagich struct {
    qulf sync.Mutex
    soni map[string]int
}

func YangiHisoblagich() *Hisoblagich {
    return &Hisoblagich{soni: make(map[string]int)}
}

func (h *Hisoblagich) Oshir(kalit string) {
    h.qulf.Lock()
    defer h.qulf.Unlock()
    h.soni[kalit]++
}

func (h *Hisoblagich) Qiymat(kalit string) int {
    h.qulf.Lock()
    defer h.qulf.Unlock()
    return h.soni[kalit]
}

func main() {
    h := YangiHisoblagich()
    var kutuvchi sync.WaitGroup

    for range 500 {
        kutuvchi.Add(1)
        go func() {
            defer kutuvchi.Done()
            h.Oshir("sorov")
        }()
    }
    kutuvchi.Wait()

    fmt.Println(h.Qiymat("sorov"))
}
Natija
500

Mutex - "bir vaqtda faqat bitta gorutina kirsin" degan qulf.

Lock() va Unlock() orasidagi kod himoyalangan bo'ladi. defer bilan yozish muhim: funksiya qanday tugashidan qat'i nazar, qulf ochiladi.

Qulfni struktura bilan birga saqlang

E'tibor bering: qulf maydoni himoyalayotgan soni maydoni bilan bitta strukturada turadi.

Bu ataylab: qulf va u himoyalaydigan ma'lumot doim yonma-yon bo'lishi kerak. Aks holda kimdir ma'lumotni qulfsiz ishlatib yuborishi mumkin.

Va yana: Hisoblagich metodlari ko'rsatkich resiverli - 12-bo'limdagi qoida. sync.Mutex ni nusxalab bo'lmaydi, nusxa boshqa qulf bo'lib qoladi. go vet bu xatoni ham topadi.

Faqat o'qish uchun RWMutex #

Go
package main

import (
    "fmt"
    "sync"
)

type Ombor struct {
    qulf    sync.RWMutex
    narxlar map[string]int
}

func (o *Ombor) Yoz(nom string, narx int) {
    o.qulf.Lock()
    defer o.qulf.Unlock()
    o.narxlar[nom] = narx
}

func (o *Ombor) Oqi(nom string) int {
    o.qulf.RLock()
    defer o.qulf.RUnlock()
    return o.narxlar[nom]
}

func main() {
    o := &Ombor{narxlar: make(map[string]int)}
    o.Yoz("olma", 12000)

    var kutuvchi sync.WaitGroup
    natijalar := make([]int, 100)

    for i := range 100 {
        kutuvchi.Add(1)
        go func() {
            defer kutuvchi.Done()
            natijalar[i] = o.Oqi("olma")
        }()
    }
    kutuvchi.Wait()

    hammasiTogri := true
    for _, n := range natijalar {
        if n != 12000 {
            hammasiTogri = false
        }
    }
    fmt.Println(hammasiTogri)
}
Natija
true

sync.RWMutex ikki xil qulf beradi:

MetodMa'nosi
Lock / UnlockYozish - bittagina gorutina
RLock / RUnlockO'qish - bir nechta birga

O'qish yozishdan ancha ko'p bo'lgan holatlarda bu sezilarli tezlik beradi.

Sikl o'zgaruvchisi - endi xavfsiz #

Go
package main

import (
    "fmt"
    "slices"
    "sync"
)

func main() {
    var kutuvchi sync.WaitGroup
    var qulf sync.Mutex
    yiqilgan := []int{}

    for i := range 5 {
        kutuvchi.Add(1)
        go func() {
            defer kutuvchi.Done()
            qulf.Lock()
            yiqilgan = append(yiqilgan, i)
            qulf.Unlock()
        }()
    }
    kutuvchi.Wait()

    slices.Sort(yiqilgan)
    fmt.Println(yiqilgan)
}
Natija
[0 1 2 3 4]
Bu misol eski Go da buzuq edi

7-bo'limda ko'rgan o'zgarish aynan shu yerda eng ko'p bilinardi.

Go 1.21 va undan oldin hamma gorutina bitta i ga bog'lanardi. Natijada ko'pincha [5 5 5 5 5] chiqardi.

Buni tuzatish uchun i := i deb nusxa olish yoki go func(n int) deb argument sifatida uzatish kerak edi.

Go 1.22 dan beri har bir aylanish o'z i sini oladi va bu xato butunlay yo'qoldi. Eski kodni o'qiyotganda shu farqni yodda tuting.

E'tibor bering: slices.Sort baribir kerak - gorutinalar tartibi hali ham kafolatlanmaydi.

Gorutina oqib ketishi #

Tugamagan gorutina xotirani band qiladi

Gorutina o'z-o'zidan to'xtamaydi. Agar u biror narsani abadiy kutib qolsa, dastur tugaguncha xotirada qoladi:

Go
func oqadi() {
    kanal := make(chan int)
    go func() {
        qiymat := <-kanal    // hech kim yubormaydi
        fmt.Println(qiymat)
    }()
    // funksiya tugadi, gorutina esa abadiy kutmoqda
}

Bu gorutina oqishi (goroutine leak). Serverda har bir so'rov bitta gorutina qoldirsa, xotira asta-sekin to'lib boradi.

Qoida: har bir gorutina uchun "u qanday tugaydi?" degan savolga javobingiz bo'lsin. Keyingi bo'limdagi context va kanalni yopish shu savolga javob beradi.

Amaliy topshiriq
  1. go bilan funksiya ishga tushiring va hech narsa chiqmasligini ko'ring.
  2. WaitGroup qo'shib, natijani oling.
  3. Add va Done sonini teng qilmay ko'ring.
  4. Ming ta gorutina ishga tushiring.
  5. Qulfsiz hisoblagich yozib, -race bilan ishga tushiring.
  6. sync.Mutex bilan uni to'g'rilang.
  7. Qulf va ma'lumotni bitta strukturaga joylang.
  8. RWMutex bilan o'qishni parallel qiling.
  9. Natijalarni indeks bo'yicha slicega yig'ing.
  10. Gorutinalar tartibi har safar o'zgarishini kuzating.

Xulosa #

  • go funksiya() - gorutinani ishga tushiradi va darhol davom etadi.
  • main tugasa, hamma gorutina o'ladi; time.Sleep bilan kutish - yomon yechim.
  • sync.WaitGroup - Add, Done, Wait uchligi bilan to'g'ri kutish usuli.
  • Gorutina arzon: bir necha kilobayt, o'n minglabi oddiy hol.
  • xavfli++ atomar emas - bu poyga holatiga olib keladi.
  • go test -race poygalarni topadi; uni testlarda doim ishlating.
  • sync.Mutex kodni himoyalaydi; qulfni u himoyalaydigan ma'lumot bilan bitta strukturada saqlang.
  • sync.RWMutex bir nechta o'quvchiga birga ruxsat beradi.
  • Go 1.22 dan beri sikl o'zgaruvchisi har aylanishda yangi - eski klassik xato yo'qoldi.
  • Har bir gorutina uchun "u qanday tugaydi?" degan javobingiz bo'lsin.

Keyingi bo'limda kanallarni o'rganamiz.

Xatolik topdingizmi?

Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.