O'zbek

Go'ning parallellik xususiyatlari bo'yicha to'liq qo'llanma, samarali va kengaytiriladigan ilovalar uchun gorutinlar va kanallarni amaliy misollar bilan o'rganish.

Go parallelligi: Gorutinlar va kanallarning kuchini ochib berish

Ko'pincha Golang deb ataladigan Go o'zining soddaligi, samaradorligi va parallellik uchun o'rnatilgan qo'llab-quvvatlashi bilan mashhur. Parallellik dasturlarga bir vaqtning o'zida bir nechta vazifalarni bajarishga imkon beradi, bu esa unumdorlik va javob berish tezligini oshiradi. Go bunga ikkita asosiy xususiyat orqali erishadi: gorutinlar va kanallar. Ushbu blog posti ushbu xususiyatlarni har tomonlama o'rganib chiqadi va barcha darajadagi dasturchilar uchun amaliy misollar va tushunchalarni taqdim etadi.

Parallellik nima?

Parallellik - bu dasturning bir nechta vazifani parallel ravishda bajarish qobiliyatidir. Parallellikni parallelizmdan farqlash muhim. Parallellik - bu bir vaqtning o'zida bir nechta vazifani *boshqarish*, parallelizm esa bir vaqtning o'zida bir nechta vazifani *bajarish* demakdir. Bitta protsessor vazifalar o'rtasida tezda almashish orqali parallellikka erishishi mumkin, bu esa bir vaqtning o'zida bajarish illyuziyasini yaratadi. Parallelizm esa, o'z navbatida, vazifalarni haqiqatan ham bir vaqtning o'zida bajarish uchun bir nechta protsessorni talab qiladi.

Restorandagi oshpazni tasavvur qiling. Parallellik - bu oshpazning sabzavotlarni to'g'rash, souslarni aralashtirish va go'shtni qovurish kabi vazifalar o'rtasida almashinib, bir nechta buyurtmalarni boshqarishiga o'xshaydi. Parallelizm esa bir nechta oshpazning har biri bir vaqtning o'zida turli buyurtmalar ustida ishlashiga o'xshaydi.

Go'ning parallellik modeli, bitta yoki bir nechta protsessorda ishlashidan qat'i nazar, parallel dasturlarni yozishni osonlashtirishga qaratilgan. Ushbu moslashuvchanlik kengaytiriladigan va samarali ilovalar yaratish uchun asosiy afzallik hisoblanadi.

Gorutinlar: Yengil vaznli oqimlar

Gorutin - bu yengil, mustaqil ishlaydigan funksiya. Uni oqim (thread) deb o'ylang, lekin u ancha samaraliroq. Gorutin yaratish juda oddiy: funksiya chaqiruvidan oldin `go` kalit so'zini qo'shish kifoya.

Gorutinlarni yaratish

Mana oddiy misol:

package main

import (
	"fmt"
	"time"
)

func sayHello(name string) {
	for i := 0; i < 5; i++ {
		fmt.Printf("Hello, %s! (Iteration %d)\n", name, i)
		time.Sleep(100 * time.Millisecond)
	}
}

func main() {
	go sayHello("Alice")
	go sayHello("Bob")

	// Gorutinlarning bajarilishiga vaqt berish uchun qisqa vaqt kuting
	time.Sleep(500 * time.Millisecond)
	fmt.Println("Main function exiting")
}

Ushbu misolda `sayHello` funksiyasi ikkita alohida gorutin sifatida ishga tushiriladi, biri "Alice" uchun, ikkinchisi "Bob" uchun. `main` funksiyasidagi `time.Sleep` asosiy funksiya tugashidan oldin gorutinlarning bajarilishiga vaqt berishini ta'minlash uchun muhimdir. Busiz, dastur gorutinlar tugamasdan oldin yakunlanishi mumkin.

Gorutinlarning afzalliklari

Kanallar: Gorutinlar o'rtasidagi aloqa

Gorutinlar kodni parallel ravishda bajarish imkonini bersa-da, ular ko'pincha bir-biri bilan aloqa qilish va sinxronlashishga muhtoj bo'ladi. Aynan shu yerda kanallar yordamga keladi. Kanal - bu gorutinlar o'rtasida qiymatlarni yuborish va qabul qilish mumkin bo'lgan tiplashtirilgan o'tkazgich.

Kanallarni yaratish

Kanallar `make` funksiyasi yordamida yaratiladi:

ch := make(chan int) // Butun sonlarni uzatuvchi kanal yaratadi

Shuningdek, buferlangan kanallar yaratishingiz mumkin, ular qabul qiluvchi tayyor bo'lmasdan turib ma'lum miqdordagi qiymatlarni saqlay oladi:

ch := make(chan int, 10) // 10 sig'imga ega buferlangan kanal yaratadi

Ma'lumotlarni yuborish va qabul qilish

Ma'lumotlar kanalga `<-` operatori yordamida yuboriladi:

ch <- 42 // 42 qiymatini ch kanaliga yuboradi

Ma'lumotlar kanaldan ham `<-` operatori yordamida qabul qilinadi:

value := <-ch // ch kanalidan qiymat qabul qilib, uni value o'zgaruvchisiga o'zlashtiradi

Misol: Gorutinlarni muvofiqlashtirish uchun kanallardan foydalanish

Mana kanallar gorutinlarni muvofiqlashtirish uchun qanday ishlatilishini ko'rsatuvchi misol:

package main

import (
	"fmt"
	"time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
	for j := range jobs {
		fmt.Printf("Worker %d started job %d\n", id, j)
		time.Sleep(time.Second)
		fmt.Printf("Worker %d finished job %d\n", id, j)
		results <- j * 2
	}
}

func main() {
	jobs := make(chan int, 100)
	results := make(chan int, 100)

	// 3 ta ishchi gorutinni ishga tushirish
	for w := 1; w <= 3; w++ {
		go worker(w, jobs, results)
	}

	// jobs kanaliga 5 ta vazifa yuborish
	for j := 1; j <= 5; j++ {
		jobs <- j
	}
	close(jobs)

	// results kanalidan natijalarni yig'ish
	for a := 1; a <= 5; a++ {
		fmt.Println("Result:", <-results)
	}
}

Ushbu misolda:

Ushbu misol kanallar ishni bir nechta gorutinlar o'rtasida taqsimlash va natijalarni yig'ish uchun qanday ishlatilishini ko'rsatadi. `jobs` kanalini yopish ishchi gorutinlarga qayta ishlanadigan boshqa vazifalar yo'qligini bildirish uchun juda muhimdir. Kanalni yopmasdan, ishchi gorutinlar cheksiz ravishda ko'proq vazifalarni kutib bloklanib qolardi.

Select operatori: Bir nechta kanallarda multiplekslash

`select` operatori bir vaqtning o'zida bir nechta kanal operatsiyalarini kutish imkonini beradi. U holatlardan biri bajarishga tayyor bo'lmaguncha bloklanadi. Agar bir nechta holat tayyor bo'lsa, ulardan biri tasodifiy tanlanadi.

Misol: Bir nechta kanallarni boshqarish uchun Select'dan foydalanish

package main

import (
	"fmt"
	"time"
)

func main() {
	c1 := make(chan string, 1)
	c2 := make(chan string, 1)

	go func() {
		time.Sleep(2 * time.Second)
		c1 <- "Message from channel 1"
	}()

	go func() {
		time.Sleep(1 * time.Second)
		c2 <- "Message from channel 2"
	}()

	for i := 0; i < 2; i++ {
		select {
		case msg1 := <-c1:
			fmt.Println("Received:", msg1)
		case msg2 := <-c2:
			fmt.Println("Received:", msg2)
		case <-time.After(3 * time.Second):
			fmt.Println("Timeout")
			return
		}
	}
}

Ushbu misolda:

`select` operatori bir nechta parallel operatsiyalarni boshqarish va bitta kanalda cheksiz bloklanib qolishning oldini olish uchun kuchli vositadir. `time.After` funksiyasi taymautlarni amalga oshirish va deadlock'larning oldini olish uchun ayniqsa foydalidir.

Go'dagi umumiy parallellik naqshlari

Go'ning parallellik xususiyatlari bir nechta umumiy naqshlarga mos keladi. Ushbu naqshlarni tushunish sizga yanada mustahkam va samarali parallel kod yozishga yordam beradi.

Ishchi pullari (Worker Pools)

Avvalgi misolda ko'rsatilganidek, ishchi pullari umumiy navbatdan (kanaldan) vazifalarni qayta ishlaydigan ishchi gorutinlar to'plamini o'z ichiga oladi. Bu naqsh ishni bir nechta protsessorlar o'rtasida taqsimlash va o'tkazuvchanlikni yaxshilash uchun foydalidir. Misollar quyidagilarni o'z ichiga oladi:

Fan-out, Fan-in (Tarqatish, Yig'ish)

Bu naqsh ishni bir nechta gorutinlarga taqsimlash (fan-out) va keyin natijalarni bitta kanalga birlashtirishni (fan-in) o'z ichiga oladi. Bu ko'pincha ma'lumotlarni parallel qayta ishlash uchun ishlatiladi.

Fan-Out: Ma'lumotlarni parallel ravishda qayta ishlash uchun bir nechta gorutinlar ishga tushiriladi. Har bir gorutin qayta ishlash uchun ma'lumotlarning bir qismini oladi.

Fan-In: Bitta gorutin barcha ishchi gorutinlardan natijalarni yig'adi va ularni bitta natijaga birlashtiradi. Bu ko'pincha ishchilardan natijalarni qabul qilish uchun kanaldan foydalanishni o'z ichiga oladi.

Misol stsenariylari:

Konveyerlar (Pipelines)

Konveyer - bu bosqichlar ketma-ketligi bo'lib, har bir bosqich oldingi bosqichdan ma'lumotlarni qayta ishlaydi va natijani keyingi bosqichga yuboradi. Bu murakkab ma'lumotlarni qayta ishlash ish oqimlarini yaratish uchun foydalidir. Har bir bosqich odatda o'z gorutinida ishlaydi va boshqa bosqichlar bilan kanallar orqali aloqa qiladi.

Misol uchun foydalanish holatlari:

Parallel Go dasturlarida xatoliklarni qayta ishlash

Xatoliklarni qayta ishlash parallel dasturlarda juda muhim. Gorutin xatolikka duch kelganda, uni to'g'ri qayta ishlash va butun dasturning ishdan chiqishini oldini olish muhimdir. Mana bir nechta eng yaxshi amaliyotlar:

Misol: Kanallar yordamida xatoliklarni qayta ishlash

package main

import (
	"fmt"
	"time"
)

func worker(id int, jobs <-chan int, results chan<- int, errs chan<- error) {
	for j := range jobs {
		fmt.Printf("Worker %d started job %d\n", id, j)
		time.Sleep(time.Second)
		fmt.Printf("Worker %d finished job %d\n", id, j)
		if j%2 == 0 { // Juft sonlar uchun xatolikni simulyatsiya qilish
			errs <- fmt.Errorf("Worker %d: Job %d failed", id, j)
			results <- 0 // O'rinbosar natija yuborish
		} else {
			results <- j * 2
		}
	}
}

func main() {
	jobs := make(chan int, 100)
	results := make(chan int, 100)
	errs := make(chan error, 100)

	// 3 ta ishchi gorutinni ishga tushirish
	for w := 1; w <= 3; w++ {
		go worker(w, jobs, results, errs)
	}

	// jobs kanaliga 5 ta vazifa yuborish
	for j := 1; j <= 5; j++ {
		jobs <- j
	}
	close(jobs)

	// Natijalar va xatolarni yig'ish
	for a := 1; a <= 5; a++ {
		select {
		case res := <-results:
			fmt.Println("Result:", res)
		case err := <-errs:
			fmt.Println("Error:", err)
		}
	}
}

Ushbu misolda biz ishchi gorutinlardan asosiy funksiyaga xato xabarlarini uzatish uchun `errs` kanalini qo'shdik. Ishchi gorutin juft sonli vazifalar uchun xatolikni simulyatsiya qiladi va `errs` kanaliga xato xabarini yuboradi. Keyin asosiy funksiya har bir ishchi gorutindan natija yoki xato qabul qilish uchun `select` operatoridan foydalanadi.

Sinxronizatsiya primitivlari: Mutekslar va WaitGroup'lar

Kanallar gorutinlar o'rtasida aloqa qilishning afzal usuli bo'lsa-da, ba'zida umumiy resurslar ustidan to'g'ridan-to'g'ri nazorat kerak bo'ladi. Go bu maqsad uchun mutekslar va waitgroup'lar kabi sinxronizatsiya primitivlarini taqdim etadi.

Mutekslar

Muteks (o'zaro istisno qulfi) umumiy resurslarni parallel kirishdan himoya qiladi. Bir vaqtning o'zida faqat bitta gorutin qulfni ushlab turishi mumkin. Bu ma'lumotlar poygalarini oldini oladi va ma'lumotlar izchilligini ta'minlaydi.

package main

import (
	"fmt"
	"sync"
)

var ( // umumiy resurs
	counter int
	m sync.Mutex
)

func increment() {
	m.Lock() // Qulfni olish
	counter++
	fmt.Println("Counter incremented to:", counter)
	m.Unlock() // Qulfni bo'shatish
}

func main() {
	var wg sync.WaitGroup

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

	wg.Wait() // Barcha gorutinlarning tugashini kutish
	fmt.Println("Final counter value:", counter)
}

Ushbu misolda `increment` funksiyasi `counter` o'zgaruvchisini parallel kirishdan himoya qilish uchun muteksdan foydalanadi. `m.Lock()` metodi hisoblagichni oshirishdan oldin qulfni oladi va `m.Unlock()` metodi hisoblagichni oshirgandan so'ng qulfni bo'shatadi. Bu bir vaqtning o'zida faqat bitta gorutin hisoblagichni oshirishini ta'minlaydi va ma'lumotlar poygalarini oldini oladi.

WaitGroup'lar

WaitGroup gorutinlar to'plamining tugashini kutish uchun ishlatiladi. U uchta metodni taqdim etadi:

Oldingi misolda `sync.WaitGroup` asosiy funksiyaning yakuniy hisoblagich qiymatini chop etishdan oldin barcha 100 gorutinning tugashini kutishini ta'minlaydi. `wg.Add(1)` har bir ishga tushirilgan gorutin uchun hisoblagichni oshiradi. `defer wg.Done()` gorutin tugagach hisoblagichni kamaytiradi va `wg.Wait()` barcha gorutinlar tugamaguncha (hisoblagich nolga yetguncha) bloklanadi.

Kontekst: Gorutinlarni boshqarish va bekor qilish

`context` paketi gorutinlarni boshqarish va bekor qilish signallarini tarqatish usulini taqdim etadi. Bu ayniqsa uzoq davom etadigan operatsiyalar yoki tashqi voqealarga asoslanib bekor qilinishi kerak bo'lgan operatsiyalar uchun foydalidir.

Misol: Bekor qilish uchun kontekstdan foydalanish

package main

import (
	"context"
	"fmt"
	"time"
)

func worker(ctx context.Context, id int) {
	for {
		select {
		case <-ctx.Done():
			fmt.Printf("Worker %d: Canceled\n", id)
			return
		default:
			fmt.Printf("Worker %d: Working...\n", id)
			time.Sleep(time.Second)
		}
	}
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())

	// 3 ta ishchi gorutinni ishga tushirish
	for w := 1; w <= 3; w++ {
		go worker(ctx, w)
	}

	// 5 soniyadan keyin kontekstni bekor qilish
	time.Sleep(5 * time.Second)
	fmt.Println("Canceling context...")
	cancel()

	// Ishchilarning chiqishiga ruxsat berish uchun bir oz kutish
	time.Sleep(2 * time.Second)
	fmt.Println("Main function exiting")
}

Ushbu misolda:

Kontekstlardan foydalanish gorutinlar kerak bo'lmaganda ularni to'g'ri yopish, resurslar oqishini oldini olish va dasturlaringiz ishonchliligini oshirish imkonini beradi.

Go parallelligining real hayotdagi qo'llanilishi

Go'ning parallellik xususiyatlari real hayotdagi keng ko'lamli ilovalarda qo'llaniladi, jumladan:

Go parallelligi uchun eng yaxshi amaliyotlar

Parallel Go dasturlarini yozishda yodda tutish kerak bo'lgan ba'zi eng yaxshi amaliyotlar:

Xulosa

Go'ning parallellik xususiyatlari, xususan gorutinlar va kanallar, parallel va bir vaqtda ishlaydigan ilovalarni yaratishning kuchli va samarali usulini taqdim etadi. Ushbu xususiyatlarni tushunib va eng yaxshi amaliyotlarga rioya qilib, siz mustahkam, kengaytiriladigan va yuqori unumdorlikka ega dasturlar yozishingiz mumkin. Ushbu vositalardan samarali foydalanish qobiliyati zamonaviy dasturiy ta'minotni ishlab chiqish, ayniqsa taqsimlangan tizimlar va bulutli hisoblash muhitlarida muhim mahoratdir. Go'ning dizayni ham tushunish oson, ham bajarish uchun samarali bo'lgan parallel kod yozishga yordam beradi.