Tarkibga o'tish

Goroutine

Goroutine — Go runtime boshqaradigan yengil bajarilish birligi. U funksiyani boshqa ishlar bilan concurrent bajarish imkonini beradi.

Backend server bir vaqtning o‘zida ko‘plab so‘rovlarni kutishi, CLI dastur bir nechta faylni qayta ishlashi yoki xizmat tashqi APIlardan ma’lumot yig‘ishi mumkin. Goroutinelar shunday mustaqil ishlarni ifodalashga yordam beradi.

Goroutine operatsion tizim threadining o‘zi emas. Go runtime ko‘plab goroutinelarni bir nechta operatsion tizim threadlari ustida rejalashtiradi. Shu sabab goroutine yaratish odatda thread yaratishdan arzonroq. Ammo goroutine bepul emas va uning hayot siklini dasturchi boshqarishi kerak.

Goroutine qanday ishga tushiriladi?

Oddiy funksiya chaqiruvi tugamaguncha keyingi qator bajarilmaydi:

work()

Chaqiruv oldiga go kalit so‘zi yozilsa, funksiya yangi goroutineda bajariladi va chaqiruvchi goroutine darhol keyingi qatorga o‘tadi:

go work()

go operatoridan keyin funksiya yoki metod chaqiruvi kelishi kerak. Oddiy qiymat yoki natijani qabul qiladigan ifoda yozib bo‘lmaydi. Funksiyaga uzatiladigan argumentlar yangi goroutine bajarila boshlashidan oldin, chaqiruvchi goroutineda baholanadi.

Birinchi misol

Quyidagi dastur yangi goroutine ishga tushiradi. done channeli ish tugaganini main funksiyasiga bildiradi.

main.go
package main

import "fmt"

func greet(name string, done chan<- struct{}) {
    fmt.Println("Salom,", name)
    done <- struct{}{}
}

func main() {
    done := make(chan struct{})

    go greet("Go", done)
    fmt.Println("Goroutine ishga tushirildi")

    <-done
    fmt.Println("Dastur tugadi")
}

Natijaning mumkin bo‘lgan ko‘rinishlaridan biri:

Goroutine ishga tushirildi
Salom, Go
Dastur tugadi

go greet("Go", done) funksiyani yangi goroutineda rejalashtiradi. main esa bajarilishni davom ettiradi. Shu sabab dastlabki ikki satrning tartibi kafolatlanmaydi: scheduler greet goroutinesini oldinroq ham bajarishi mumkin.

done <- struct{}{} ish tugaganini channelga yuboradi. <-done shu signal kelguncha main goroutinesini kutdiradi. Bo‘sh struct{} qiymati faqat signal vazifasini bajaradi. Unda foydali ma’lumot saqlanmaydi.

Oxirgi Dastur tugadi satri har doim salomlashuvdan keyin chiqadi, chunki u signal qabul qilingandan keyin bajariladi. Channel mavzusi keyingi bo‘limlarda batafsil tushuntiriladi.

main ham goroutine sifatida ishlaydi

Go dasturi main goroutinesida main() funksiyasini bajarishdan boshlanadi. main() qaytsa, protsess tugaydi. Runtime boshqa goroutinelar yakunlanishini avtomatik kutmaydi.

Quyidagi dastur ataylab ishonchsiz yozilgan:

main.go
1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
    go fmt.Println("Bu satr chiqmasligi mumkin")
    fmt.Println("main tugadi")
}

Kafolatlangan yagona satr:

main tugadi

Birinchi satr chiqishi ham, chiqmasligi ham mumkin. Yangi goroutine ishga tushishga ulgurmasdan main() qaytishi ehtimoli bor. Schedulerning ma’lum tartibda ishlashiga tayanish xato.

Nega time.Sleep yechim emas?

Namoyish uchun time.Sleep bilan mainni ushlab turish mumkin, ammo bu goroutine tugaganini bildirmaydi. Ish odatdagidan uzoqroq davom etsa, kutish vaqti yetmaydi. Ish tez tugasa, dastur ortiqcha kutadi.

Ish tugashini hodisa orqali kutish kerak:

  • faqat tugashni kutish uchun sync.WaitGroup;
  • qiymat yoki xato uzatish uchun channel;
  • bekor qilish va timeout uchun context.Context.

Navbatdagi darsda sync.WaitGroup bilan bir nechta goroutineni kutishni o‘rganamiz.

Bir nechta goroutine bilan ishlash

Tasavvur qiling, dastur uchta mustaqil xizmatdan ma’lumot oladi. Har bir xizmat uchun alohida goroutine ishga tushirish kutish vaqtlarini ustma-ust bajarish imkonini beradi.

main.go
package main

import (
    "fmt"
    "time"
)

type result struct {
    service string
    value   string
}

func fetch(service string, delay time.Duration, results chan<- result) {
    time.Sleep(delay)
    results <- result{
        service: service,
        value:   service + " ma’lumoti",
    }
}

func main() {
    services := []string{"profil", "buyurtma", "to‘lov"}
    delays := []time.Duration{
        30 * time.Millisecond,
        10 * time.Millisecond,
        20 * time.Millisecond,
    }
    results := make(chan result, len(services))

    for i, service := range services {
        go fetch(service, delays[i], results)
    }

    values := make(map[string]string, len(services))
    for range services {
        item := <-results
        values[item.service] = item.value
    }

    for _, service := range services {
        fmt.Println(values[service])
    }
}

Natija:

profil ma’lumoti
buyurtma ma’lumoti
to‘lov ma’lumoti

Har bir fetch tashqi xizmat kutishini tasvirlaydi. time.Sleep bu safar goroutineni kutish uchun emas, I/O kechikishini modellashtirish uchun ishlatilgan. Natijalar tugash tartibida channelga keladi. Dastur ularni mapga joylaydi va keyin services tartibida chiqaradi. Shu sabab ko‘rsatilgan natija barqaror.

Channel sig‘imi ishlar soniga teng. Har bir goroutine main darhol qabul qilishga ulgurmasa ham bitta natijani yubora oladi. Dastur aynan uchta ish boshlagani uchun uchta natijani kutadi. Haqiqiy loyihada natija bilan birga error ham uzatiladi va uzoq operatsiya uchun timeout yoki bekor qilish yo‘li belgilanadi.

Concurrency bu misolni avtomatik ravishda parallel hisoblashga aylantirmaydi. Goroutinelar kutish paytida bir-biriga yo‘l beradi. Ularning ayni paytda turli CPU yadrolarida bajarilishi runtime va muhitga bog‘liq.

Anonim funksiya va argumentlar

Goroutine anonim funksiya bilan ham ishga tushirilishi mumkin:

go func(id int) {
    fmt.Println("Ish:", id)
}(id)

Oxiridagi (id) anonim funksiyani chaqiradi va joriy qiymatni parametr sifatida uzatadi. Bu usul goroutine aynan qaysi qiymat bilan ishlashini ochiq ko‘rsatadi.

Sikldagi o‘zgaruvchini tashqi muhitdan to‘g‘ridan-to‘g‘ri olish eski Go versiyalarida keng tarqalgan closure xatosiga sabab bo‘lgan. Zamonaviy Go versiyalarida for siklining har iteratsiyasi uchun o‘zgaruvchi semantikasi yaxshilangan. Shunga qaramay, qiymatni parametr bilan uzatish kod niyatini aniq qiladi va eski modullar bilan ishlaganda ham xavfsizroq.

Goroutine ichidan natija qaytarish

Oddiy funksiya qaytargan qiymatni go chaqiruvidan olib bo‘lmaydi. Quyidagi yozuv kompilyatsiya qilinmaydi:

// Noto‘g‘ri misol:
// value := go calculate()

go calculate() chaqiruvchi bajarilishini kutmaydi. Natija qachon tayyor bo‘lishi noma’lum. Qiymatni channel orqali yuborish yoki sinxronizatsiya bilan himoyalangan umumiy tuzilmaga yozish kerak.

Umumiy xotiradan foydalanilganda ikki goroutine bir qiymatga bir paytda murojaat qilishi mumkin. Murojaatlardan kamida bittasi yozish bo‘lsa va ular sinxronizatsiya qilinmasa, data race yuz beradi. Bunday kodning ba’zan to‘g‘ri natija berishi uning xavfsizligini isbotlamaydi.

Race detector bajarilish davomida kuzatilgan data racelarni topishga yordam beradi:

go test -race ./...
go run -race main.go

Race detector barcha mumkin bo‘lgan bajarilish tartiblarini sinamaydi. U xato topmasa ham, umumiy xotiraga murojaat to‘g‘ri sinxronizatsiya qilinganini kod tuzilishidan tekshirish kerak.

Go runtime goroutineni qanday boshqaradi?

Har bir goroutine kichik stek bilan boshlanadi. Funksiya chaqiruvlari ko‘payganda runtime stekni kengaytirishi, ehtiyoj kamayganda esa kichraytirishi mumkin. Shu xususiyat ko‘p goroutine yaratishni katta, oldindan belgilangan stekli threadlarga qaraganda arzonlashtiradi.

Aniq boshlang‘ich stek hajmiga dastur mantig‘ini bog‘lamang. Bu runtime implementatsiyasining vaqt o‘tishi bilan o‘zgarishi mumkin bo‘lgan tafsiloti.

Scheduler goroutinelarni operatsion tizim threadlari ustida taqsimlaydi. Goroutine channel, mutex, taymer yoki ayrim I/O operatsiyasi sabab bloklansa, runtime o‘sha threadda boshqa tayyor goroutineni bajarishi mumkin. Scheduler bajarilish tartibini kafolatlamaydi.

Goroutine almashinuvi ham xarajatsiz emas. Runtime bajarilish holatini saqlaydi, navbatlarni boshqaradi va steklarni kuzatadi. Millionlab mayda ishlarni alohida goroutinega ajratish doim samarali yechim bo‘lavermaydi.

Goroutine hayot sikli

Har bir ishga tushirilgan goroutine uchun to‘rtta savolga javob bo‘lishi kerak:

  1. U qachon tugaydi?
  2. Natija yoki xato qayerga uzatiladi?
  3. Kutish qanday bekor qilinadi?
  4. U ishlatadigan resurslarni kim yopadi?

Goroutine funksiyasi return qilsa yoki oxiriga yetsa, goroutine tugaydi. Uni tashqaridan majburan o‘ldiradigan xavfsiz operator yo‘q. Bekor qilish hamkorlik asosida ishlaydi: goroutine channel yoki context.Context orqali signal oladi va o‘zi return qiladi.

Goroutine leak

Goroutine hech qachon kelmaydigan channel qiymatini, yuborib bo‘lmaydigan channel qabul qiluvchisini yoki tugamaydigan I/O operatsiyasini kutib qolsa, goroutine leak yuz beradi. Goroutine foydali ish bajarmasa ham stek va bog‘langan resurslarni saqlab turadi.

Uzoq ishlaydigan operatsiyaga timeout va bekor qilish yo‘li bering. Channel yuborish yoki qabul qilish abadiy bloklanmasligini tekshiring. Serverda so‘rov tugagach, unga tegishli goroutinelar ham tugashi kerak.

Goroutine ichidagi panic

Bitta goroutinedagi ushlanmagan panic faqat o‘sha goroutineni emas, butun dasturni to‘xtatadi. recover faqat shu goroutineda bajarilayotgan deferred funksiya ichida panicni ushlay oladi. Boshqa goroutinedagi recover yordam bermaydi.

Har joyda recover ishlatish xatoni yashiradi. U odatda server yoki worker kabi aniq chegarada panicni qayd etish, resurslarni yopish va boshqariladigan javob qaytarish uchun qo‘llanadi.

Keng tarqalgan xatolar

Goroutinelar tartib bilan ishlaydi deb o‘ylash

go first() qatori go second()dan oldin yozilgani first oldin tugashini anglatmaydi. Tartib muhim bo‘lsa, channel yoki boshqa sinxronizatsiya vositasi bilan bog‘lanishni ochiq ifodalang.

Cheksiz goroutine yaratish

Har bir kelgan element uchun nazoratsiz goroutine yaratish xotira, ma’lumotlar bazasi ulanishlari yoki tashqi API limitini tugatishi mumkin. Bir paytda bajariladigan ishlar sonini worker pool yoki semaphore bilan cheklang.

Xato va natijani yo‘qotish

Goroutine chaqiruvchiga oddiy return orqali natija qaytara olmaydi. Natija bilan birga xatoni ham channel yoki himoyalangan tuzilma orqali uzating. Bir ish xato qilganda qolgan ishlar davom etishi yoki bekor qilinishi kerakligini oldindan belgilang.

time.Sleep bilan sinxronlashtirish

Taxminiy kutish testlarni sekin va beqaror qiladi. Ish tugashini WaitGroup yoki channel bilan, vaqt chegarasini esa timeout bilan ifodalang.

Umumiy o‘zgaruvchiga himoyasiz yozish

Bir nechta goroutine map, slice elementi yoki hisoblagichga yozsa, sinxronizatsiya talab qilinishi mumkin. Channel orqali egalikni bitta goroutinega berish yoki sync.Mutex kabi mos vositadan foydalanish kerak.

Interviewda nimalarga e’tibor beriladi?

  • goroutine OS threadi emas, uni Go runtime boshqaradi;
  • go f() funksiyani rejalashtiradi va chaqiruvchi kutmasdan davom etadi;
  • main() qaytsa, boshqa goroutinelar avtomatik kutilmaydi;
  • goroutine natijasi va xatosi alohida uzatiladi;
  • bajarilish tartibi kafolatlanmaydi;
  • goroutine leak, data race va nazoratsiz fan-out real loyihalardagi asosiy xavflardir;
  • goroutineni tashqaridan majburan to‘xtatish o‘rniga cancellation signali bilan hamkorlikda yakunlash kerak.

Xulosa

Goroutine funksiyani boshqa ishlar bilan concurrent bajarishga imkon beradi. Uni go kalit so‘zi bilan boshlaysiz, ammo tugashini kutish, natija va xatoni olish hamda bekor qilish yo‘lini o‘zingiz loyihalaysiz.

Scheduler tartibiga yoki time.Sleepga tayanmang. Umumiy xotirani himoya qiling va bir paytda yaratiladigan goroutinelar sonini nazorat qiling. Keyingi darsda sync.WaitGroup yordamida bir nechta goroutinening tugashini ishonchli kutishni ko‘rib chiqamiz.