Dasturchilar ko'pincha Data Race va Race Condition atamalarini bitta narsa deb adashtiradilar. Aslida esa ular tubdan farq qiladi: dasturingizda hech qanday ma'lumotlar poygasi (data race) bo'lmasligi, lekin shunga qaramay jiddiy mantiqiy poyga (race condition) tufayli noto'g'ri ishlashi mumkin!
go run -race orqali aniqlanadi.Keling, bank hisobidan pul yechish misolini ko'raylik:
type Account struct {
mu sync.Mutex
balance int
}
func (a *Account) Balance() int {
a.mu.Lock()
defer a.mu.Unlock()
return a.balance
}
func (a *Account) Withdraw(amount int) {
a.mu.Lock()
defer a.mu.Unlock()
a.balance -= amount
}
Bu yerda har bir metod alohida Mutex bilan himoyalangan, shuning uchun Data Race yo'q.
Endi quyidagi kodni ko'ring:
// Agar pul yetarli bo'lsa, pul yechilsin:
if account.Balance() >= 100 { // (1) Check (Tekshirish)
// Aynan shu yerda boshqa goroutine pulni yechib ketishi mumkin!
account.Withdraw(100) // (2) Act (Amalni bajarish)
}
Mana bu haqiqiy **Race Condition (Check-then-act)**dir:
100 bor, yetarli).100 bor, yetarli).0 bo'ldi).-100) tushib ketadi!Yechim: Tekshirish va yechish amallari bitta atomik metod ichida, yagona qulf ostida birlashtirilishi kerak:
func (a *Account) TryWithdraw(amount int) bool {
a.mu.Lock()
defer a.mu.Unlock()
if a.balance >= amount {
a.balance -= amount
return true // muvaffaqiyatli
}
return false // mablag' yetarli emas
}
Go 1.18 dan boshlab sync.Mutexga TryLock() metodi qo'shildi. U qulf bo'sh bo'lsa darhol egallab true qaytaradi, aks holda kutmasdan false beradi.
[!WARNING]
TryLockni sikl ichida kutish (busy loop) uchun ishlatmang! Bu protsessorni 100% yuklab qo'yadi.TryLockfaqatgina "agar hozir qulf bo'sh bo'lsa amalni bajar, band bo'lsa keyinroqqa qoldir yoki umuman bajarma" degan juda kam uchraydigan holatlar uchun mo'ljallangan.
Go tilining shiori:
"Do not communicate by sharing memory; instead, share memory by communicating." (Xotirani ulashish orqali aloqa qilmang; aloqa qilish orqali xotirani ulashing).
Eng xavfsiz concurrency — bu bir nechta goroutine'lar o'rtasida umumiy o'zgaruvchilar bo'lmasligi (Shared Nothing). Har bir goroutine o'z ma'lumotlariga mustaqil egalik qilsin va faqat kanallar orqali tayyor natijalarni uzatsin.
Yuqoridagi TryWithdraw andozasidan foydalanib, balans minusga tushib ketmasligini kafolatlovchi xavfsiz bank hisobini tuzing.
package main
import (
"fmt"
"sync"
)
type BankAccount struct {
mu sync.Mutex
balance int
}
func (b *BankAccount) Withdraw(amount int) bool {
// TODO: Balans yetarli bo'lsa yechib true qaytaring, aks holda false
return false
}
func main() {
acc := &BankAccount{balance: 100}
var wg sync.WaitGroup
// Bir vaqtda ikkita ishchi 100 so'm yechishga urinadi:
for i := 0; i < 2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
if acc.Withdraw(100) {
fmt.Printf("Mijoz #%d: 100 so'm yechildi!
", id)
} else {
fmt.Printf("Mijoz #%d: Mablag' yetarli emas!
", id)
}
}(i)
}
wg.Wait()
fmt.Println("Yakuniy balans:", acc.balance)
}
Sinxronizatsiyada faqat Data Race'dan emas, balki mantiqiy Race Condition'dan ham ehtiyot bo'lish zarur.
Keyingi mavzu: Semaphores (Semaforlar va Rendezvous andozasi)!