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, 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.

challenge icon

Görev

Kolay

Yapı (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, bir balance alanı (int) ve tüm başarılı işlemleri dize (string) olarak kaydeden bir transactions dilimi (slice) içeren bir BankAccount yapı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şturur
    • Deposit(amount int) - Tutarı bakiyeye ekler ve işlemi +[amount] olarak kaydeder
    • Withdraw(amount int) bool - Yeterli bakiye varsa tutarı düşer, -[amount] olarak kaydeder ve true döner. Aksi takdirde hiçbir şeyi değiştirmeden false döner
    • Balance() int - Mevcut bakiyeyi döner
    • History() []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 defer kullanı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, withdraw veya balance) 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ı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, 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, withdraw veya balance) 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
-30

Buradaki 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 defer kullanı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.RWMutex kullanın ve okumalar için RLock() ç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
}
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