Thread-Safe Struct Tasarımı
Coddy'nin GO Journey'sinin Nesne Yönelimli Programlama bölümünün bir parçası — ders 65 / 107.
Artık mutex'leri ve WaitGroup'ları anladığınıza göre, aynı anda birden fazla goroutine'den kullanımı güvenli olan struct'lar tasarlamak için bunları birleştirelim. Bir thread-safe struct, senkronizasyonu kendi metotları içinde kapsüller, böylece çağıranların kilitleme (locking) konusunda endişelenmesine gerek kalmaz.
Desen oldukça basittir: yapınıza (struct) bir mutex gömün ve paylaşılan duruma erişen her metotta onu kilitleyin:
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}Salt okunur Value() metodunun bile mutex'i kilitlediğine dikkat edin. Bu olmadan, bir goroutine yazma işlemi yaparken diğeri okuma yapabilir ve bu da bir veri yarışına (data race) yol açabilir. Eğer okuma işlemleri yazma işlemlerinden çok daha yaygınsa, bunun yerine sync.RWMutex kullanın ve okumalar için RLock() metodunu çağırın.
Temel bir tasarım ilkesi: mutex'i gizli tutun. Küçük harfle başlayan bir alan adı (mu) kullanarak, harici kodun ona doğrudan erişmesini engellersiniz. Tüm senkronizasyon metotlarınız aracılığıyla gerçekleşir, bu da size iş parçacığı güvenliği (thread safety) üzerinde tam kontrol sağlar.
Birden fazla alanı olan struct'lar için, tutarlı bir durum sağlamak amacıyla ilgili tüm alanları aynı mutex ile koruyun:
type Account struct {
mu sync.Mutex
balance int
history []string
}
func (a *Account) Deposit(amount int) {
a.mu.Lock()
defer a.mu.Unlock()
a.balance += amount
a.history = append(a.history, fmt.Sprintf("+%d", amount))
}Hem balance hem de history atomik olarak güncellenir—hiçbir goroutine, birinin değiştiği ancak diğerinin değişmediği tutarsız bir durumu gözlemleyemez.
Görev
KolayYapı (struct) metotları içinde senkronizasyonun düzgün bir şekilde kapsüllenmesini (encapsulation) gösteren, iş parçacığı açısından güvenli (thread-safe) bir banka hesabı sistemi oluşturalım. Hesabınız, çağıranlara herhangi bir kilitleme detayı ifşa etmeden eşzamanlı para yatırma, çekme ve bakiye kontrollerini güvenli bir şekilde gerçekleştirecektir.
Kodunuzu iki dosya halinde düzenleyeceksiniz:
account.go: İş parçacığı açısından güvenli banka hesabınızı tanımlayın.Gömülü bir
sync.Mutex, birbalancealanı (int) ve tüm başarılı işlemleri dize (string) olarak kaydeden birtransactionsdilimi (slice) içeren birBankAccountyapısı oluşturun.Şu metotları uygulayın:
NewBankAccount(initial int) *BankAccount- Verilen başlangıç bakiyesi ve boş bir işlem dilimi ile yeni bir hesap oluştururDeposit(amount int)- Tutarı bakiyeye ekler ve işlemi+[amount]olarak kaydederWithdraw(amount int) bool- Yeterli bakiye varsa tutarı düşer,-[amount]olarak kaydeder vetruedöner. Aksi takdirde hiçbir şeyi değiştirmedenfalsedönerBalance() int- Mevcut bakiyeyi dönerHistory() []string- İşlem diliminin bir kopyasını döner
Yapının alanlarına erişen her metot, iş parçacığı güvenliğini sağlamak için mutex'i kilitlemelidir. Kilidi açmak için
deferkullanın. Mutex'i ve tüm alanları dışa aktarılmamış (küçük harf) tutun, böylece harici kodlar metotlarınızı kullanmak zorunda kalır.main.go: Bankacılık işlemlerini işleyin ve iş parçacığı açısından güvenli hesabınızı gösterin.Başlangıç bakiyesini ve ardından işlem sayısını okuyun. Her işlem için türü (
deposit,withdrawveyabalance) ve para yatırma/çekme için tutarı okuyun.Her işlem için sonuçları yazdırın:
deposit:Deposited [amount], Balance: [new balance]yazdırınwithdraw: BaşarılıysaWithdrew [amount], Balance: [new balance], değilseWithdrawal failed: insufficient fundsyazdırınbalance:Current balance: [balance]yazdırın
Tüm işlemlerden sonra, her bir girdi yeni bir satırda olacak şekilde işlem geçmişini yazdırın; yalnızca ilk girdinin başına
History:ekleyin.
Aşağıdaki girdiler sağlanacaktır:
- Satır 1: Başlangıç bakiyesi (tamsayı)
- Satır 2: İşlem sayısı (tamsayı)
- Sonraki satırlar: Her işlem için tür (
deposit,withdrawveyabalance) ve para yatırma/çekme için bir sonraki satırda tutar
Örneğin, şu girdi verildiğinde:
100
5
deposit
50
balance
withdraw
30
withdraw
200
balanceÇıktınız şu şekilde olmalıdır:
Deposited 50, Balance: 150
Current balance: 150
Withdrew 30, Balance: 120
Withdrawal failed: insufficient funds
Current balance: 120
History: +50
-30Buradaki temel ilke, tüm senkronizasyonun BankAccount metotlarınızın içinde gizli olmasıdır. Çağıranlar, kilitler hakkında hiç düşünmeden sadece Deposit(), Withdraw() ve Balance() metotlarını kullanır; yapınız iş parçacığı güvenliğini dahili olarak yönetir.
Kopya kağıdı
İş parçacığı güvenli (thread-safe) bir yapı, bir mutex ekleyerek ve paylaşılan duruma erişen her yöntemde bu mutex'i kilitleyerek senkronizasyonu kendi yöntemleri içinde kapsüller:
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}Temel ilkeler:
- Veri yarışlarını (data races) önlemek için, salt okunur yöntemler de dahil olmak üzere paylaşılan duruma erişen tüm yöntemlerde mutex'i kilitleyin
- Fonksiyon erken dönse bile mutex'in kilidinin açıldığından emin olmak için
deferkullanın - Dış kodun doğrudan erişememesi için mutex'i özel (private) tutun (küçük harfli alan adı)
- Okuma ağırlıklı iş yükleri için
sync.RWMutexkullanın ve okumalar içinRLock()çağırın
Birden fazla alanı olan yapılar için, tutarlı bir durum sağlamak amacıyla ilgili tüm alanları aynı mutex ile koruyun:
type Account struct {
mu sync.Mutex
balance int
history []string
}
func (a *Account) Deposit(amount int) {
a.mu.Lock()
defer a.mu.Unlock()
a.balance += amount
a.history = append(a.history, fmt.Sprintf("+%d", amount))
}Bu, hem balance hem de history alanlarının atomik olarak güncellenmesini sağlar; hiçbir goroutine tutarsız bir durum gözlemleyemez.
Kendin dene
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Başlangıç bakiyesini oku
initialStr, _ := reader.ReadString('\n')
initial, _ := strconv.Atoi(strings.TrimSpace(initialStr))
// İşlem sayısını oku
numOpsStr, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(numOpsStr))
// Banka hesabını oluştur
account := NewBankAccount(initial)
// Her bir işlemi işle
for i := 0; i < numOps; i++ {
opType, _ := reader.ReadString('\n')
opType = strings.TrimSpace(opType)
switch opType {
case "deposit":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: Deposit fonksiyonunu çağır ve sonucu yazdır
// Format: "Deposited [amount], Balance: [new balance]"
case "withdraw":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: Withdraw fonksiyonunu çağır ve uygun sonucu yazdır
// Başarılıysa: "Withdrew [amount], Balance: [new balance]"
// Başarısızsa: "Withdrawal failed: insufficient funds"
_ = amount // Uyguladığınızda bu satırı kaldırın
case "balance":
// TODO: Balance fonksiyonunu çağır ve sonucu yazdır
// Format: "Current balance: [balance]"
}
}
// TODO: İşlem geçmişini yazdır
// İlk girişin başına "History: " eklenmelidir
// Sonraki girişler önek olmadan yeni satırlarda olmalıdır
}
Bu ders kısa bir quiz içerir. Soruları yanıtlamak ve ilerlemeni kaydetmek için derse başla.
Nesne Yönelimli Programlama bölümündeki tüm dersler
1Go OOP Temelleri
Harici DosyalarGo Çalışma Alanı ve ModüllerPaketler ve İçe AktarmalarDışa Aktarılan ve Aktarılmayan İsimlerGo'da OOP'ye GirişSınıf Olarak Struct'larStruct'larda Metot TanımlamaPointer ve Değer AlıcılarStruct BaşlatmaYapıcı FonksiyonlarÖzet - Basit Hesap Makinesi4Arayüzler
Arayüzlere GirişÖrtük UygulamaSözleşme Olarak ArayüzBoş Arayüz (any)Tür OnaylamaTür SeçimiArayüz BileşimiStringer ve Error ArayüzleriÖzet - Şekil Hesaplayıcı7Kapsülleme
Dışa Aktarılan ve Aktarılmayan AlanlarPaket Seviyesinde KapsüllemeGetter ve Setter MetotlarıGo'da Bilgi GizlemeÖzet - Öğrenci Kayıtları10Generics (Go 1.18+)
Generics'e GirişTür ParametreleriTür KısıtlamalarıGeneric Struct'larGeneric Metotlar İçin Geçici ÇözümÖzet - Generic Koleksiyon13Tasarım Kalıpları Bölüm 1
Tasarım Kalıplarına GirişSingleton KalıbıFactory KalıbıAbstract Factory KalıbıObserver KalıbıStrategy Kalıbı2Türler ve Struct'lar: Derinlemesine İnceleme
Temel ve Bileşik TürlerÖzel Tür TanımlamalarıStruct EtiketleriAnonim Struct'larİç İçe Geçmiş Struct'larSıfır Değerler ve VarsayılanlarÖzet - Rehber5Kalıtım Yerine Kompozisyon
Go'da Neden Kalıtım YokStruct Gömme TemelleriMetot YükseltmeBirden Fazla Struct GömmeGömme vs AgregasyonGömülü Metotları GölgelemeÖzet - Çalışan Hiyerarşisi8Hata Yönetimi ve OOP
error ArayüzüÖzel Hata TürleriHata Sarmalama (fmt.Errorf)Sentinel Hatalarerrors.Is() ve errors.As()Panic, Defer ve RecoverÖzet - Dosya Ayrıştırıcı3İşaretçiler ve Bellek
Go'da İşaretçi TemelleriStruct İşaretçileriDeğer ile Geçme ve Referans ile Geçmenew() FonksiyonuGo'da Garbage CollectionÖzet - Linked List Oluşturucu6Go'da Polimorfizm
Interface'ler ile PolimorfizmGo'da Duck TypingInterface Karşılama KurallarıPolimorfik KoleksiyonlarDependency InjectionÖzet - Payment Processor9Eşzamanlılık ve OOP
Goroutine TemelleriKanallar ve İletişimTamponlu ve Tamponsuz KanallarSelect İfadesisync.Mutex ve sync.RWMutexsync.WaitGroupThread-Safe Struct TasarımıÖzet - Worker Pool