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, birden çok goroutine tarafından aynı anda güvenle kullanılabilecek struct'lar tasarlamak için bunları birleştirelim. İş parçacığı güvenli bir struct, senkronizasyonu kendi metotları içinde kapsüller; böylece çağıranların kilitleme konusunda endişelenmesine gerek kalmaz.
Kalıp basittir: yapınıza bir mutex yerleştirin ve paylaşılan duruma erişen her yöntemde 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() yönteminin bile mutex'i kilitlediğine dikkat edin. Bu olmazsa, bir goroutine diğeri yazma işlemi yaparken okuma yapabilir ve bu da bir veri yarışına neden olur. Okuma işlemleri yazma işlemlerinden çok daha yaygınsa, bunun yerine sync.RWMutex kullanın ve okumalar için RLock() çağırın.
Temel bir tasarım ilkesi: mutex'i private tutun. Küçük harfli bir alan adı (mu) kullanarak harici kodun bu alana doğrudan erişmesini önlersiniz. Tüm senkronizasyon metotlarınız aracılığıyla gerçekleşir; bu da iş parçacığı güvenliği üzerinde tam denetim sağlar.
Birden fazla alanı olan yapıların tutarlı bir duruma sahip olmasını sağlamak için 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şip diğerinin değişmediği tutarsız bir durumu gözlemleyemez.
Görev
KolayYapı metotları içinde senkronizasyonun uygun şekilde kapsüllenmesini gösteren iş parçacığı güvenli bir banka hesabı sistemi oluşturalım. Hesabınız, çağıranlara herhangi bir kilitleme ayrıntısı göstermeden eşzamanlı para yatırma, para çekme ve bakiye kontrollerini güvenli bir şekilde gerçekleştirecek.
Kodunuzu iki dosya arasında düzenleyeceksiniz:
account.go: İş parçacığı güvenli banka hesabını tanımlayın.İçinde
BankAccountbulunan, birsync.Mutexalanına (int) ve tüm başarılı işlemleri dizeler olarak kaydeden birbalancedilimine sahip birtransactionsyapısı oluşturun.Şu metotları uygulayın:
NewBankAccount(initial int) *BankAccount- Verilen başlangıç bakiyesiyle ve boş bir transactions dilimiyle yeni bir hesap oluştururDeposit(amount int)- Tutarı bakiyeye ekler ve işlemi+[amount]olarak kaydederWithdraw(amount int) bool- Yeterli bakiye varsa tutarı çıkarır,-[amount]kaydını oluşturur vetruedöndürür. Aksi halde hiçbir şeyi değiştirmedenfalsedöndürürBalance() int- Geçerli bakiyeyi döndürürHistory() []string- transactions diliminin bir kopyasını döndürür
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 harfle başlayan) tutun; böylece harici kodun metotlarınızı kullanması gerekir.main.go: Bankacılık işlemlerini işleyin ve iş parçacığı güvenli hesabınızı gösterin.Başlangıç bakiyesini, ardından işlem sayısını okuyun. Her işlem için türü (
deposit,withdrawveyabalance) okuyun; deposit/withdraw işlemleri için tutarı da 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 işlem geçmişini yazdırın; her girdi yeni bir satırda olmalı ve yalnızca ilk girdenin önünde
History:bulunmalıdır.
Aşağıdaki girdiler sağlanacaktır:
- 1. satır: Başlangıç bakiyesi (tam sayı)
- 2. satır: İşlem sayısı (tam sayı)
- Sonraki satırlar: Her işlem için tür (
deposit,withdrawveyabalance); deposit/withdraw için tutar sonraki satırda verilir
Örneğin, şu verildiğinde:
100
5
deposit
50
balance
withdraw
30
withdraw
200
balanceÇıktınız şu 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 gizlenmesidir. Çağıranlar kilitleri hiç düşünmeden yalnızca Deposit(), Withdraw() ve Balance() kullanır. Yapınız iş parçacığı güvenliğini dahili olarak yönetir.
Kendin dene
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// İlk bakiyeyi 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 işlemi gerçekleştir
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'i çağır ve sonucu yazdır
// Biçim: "Deposited [amount], Balance: [new balance]"
case "withdraw":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: Withdraw'ı çağır ve uygun sonucu yazdır
// Başarılı olursa: "Withdrew [amount], Balance: [new balance]"
// If failed: "Withdrawal failed: insufficient funds"
_ = amount // Uyguladığınızda bu satırı kaldırın
case "balance":
// TODO: Balance'ı çağır ve sonucu yazdır
// Format: "Current balance: [balance]"
}
}
// TODO: İşlem geçmişini yazdır
// İlk girdi "History: " öneki ile başlamalı
// Sonraki girdiler ö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 PoolKendi başına pratik yap: Online Go derleyicisi