Menu
Coddy logo textTech

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.

challenge icon

Görev

Kolay

Yapı 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 BankAccount bulunan, bir sync.Mutex alanına (int) ve tüm başarılı işlemleri dizeler olarak kaydeden bir balance dilimine sahip bir transactions yapısı oluşturun.

    Şu metotları uygulayın:

    • NewBankAccount(initial int) *BankAccount - Verilen başlangıç bakiyesiyle ve boş bir transactions dilimiyle yeni bir hesap oluşturur
    • Deposit(amount int) - Tutarı bakiyeye ekler ve işlemi +[amount] olarak kaydeder
    • Withdraw(amount int) bool - Yeterli bakiye varsa tutarı çıkarır, -[amount] kaydını oluşturur ve true döndürür. Aksi halde hiçbir şeyi değiştirmeden false döndürür
    • Balance() int - Geçerli bakiyeyi döndürür
    • History() []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 defer kullanı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, withdraw veya balance) 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ın
    • withdraw: Başarılıysa Withdrew [amount], Balance: [new balance], değilse Withdrawal failed: insufficient funds yazdırın
    • balance: 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, withdraw veya balance); 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
-30

Buradaki 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
}
quiz iconKendini test et

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

Kendi başına pratik yap: Online Go derleyicisi