Menu
CoddyTech

Desain Struct yang Aman untuk Thread

Termasuk bagian Pemrograman Berorientasi Objek dalam perjalanan GO di Coddy. Pelajaran 65 dari 107.

Sekarang setelah kamu memahami mutex dan WaitGroup, mari gabungkan keduanya untuk merancang struct yang aman digunakan secara bersamaan oleh beberapa goroutine. Struct yang aman untuk thread mengenkapsulasi sinkronisasi di dalam method-nya, sehingga pemanggil tidak perlu khawatir tentang penguncian.

Polanya sederhana: sematkan mutex di struct-mu dan kunci mutex itu di setiap method yang mengakses status bersama:

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
}

Perhatikan bahwa bahkan method Value() yang hanya bisa membaca tetap mengunci mutex. Tanpa ini, satu goroutine mungkin membaca saat goroutine lain menulis, sehingga menyebabkan data race. Jika pembacaan jauh lebih sering daripada penulisan, gunakan sync.RWMutex sebagai gantinya dan panggil RLock() untuk membaca.

Prinsip desain utama: jaga mutex tetap privat. Dengan menggunakan nama field huruf kecil (mu), kamu mencegah kode eksternal mengaksesnya secara langsung. Semua sinkronisasi terjadi melalui method-mu, sehingga kamu memiliki kendali penuh atas keamanan thread.

Untuk struct dengan beberapa field, lindungi semua field terkait dengan mutex yang sama untuk memastikan status tetap konsisten:

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))
}

balance dan history diperbarui secara atomik: tidak ada goroutine yang dapat melihat keadaan yang tidak konsisten, yaitu saat salah satunya berubah tetapi yang lain tidak.

challenge icon

Tantangan

Mudah

Mari kita buat sistem rekening bank yang aman untuk thread, yang menunjukkan enkapsulasi sinkronisasi yang tepat di dalam method struct. Rekeningmu akan menangani setoran, penarikan, dan pemeriksaan saldo secara bersamaan dengan aman tanpa mengungkap detail penguncian kepada pemanggil.

Kamu akan menyusun kodemu dalam dua file:

  • account.go: Definisikan rekening bank yang aman untuk thread.

    Buat struct BankAccount dengan sync.Mutex yang disematkan, field balance (int), dan slice transactions yang mencatat semua operasi berhasil sebagai string.

    Implementasikan method berikut:

    • NewBankAccount(initial int) *BankAccount: Membuat rekening baru dengan saldo awal yang diberikan dan slice transactions yang kosong
    • Deposit(amount int): Menambahkan jumlah tersebut ke saldo dan mencatat transaksi sebagai +[amount]
    • Withdraw(amount int) bool: Jika dana mencukupi, mengurangi jumlah tersebut, mencatat -[amount], dan mengembalikan true. Jika tidak, mengembalikan false tanpa mengubah apa pun
    • Balance() int: Mengembalikan saldo saat ini
    • History() []string: Mengembalikan salinan slice transactions

    Setiap method yang mengakses field struct harus mengunci mutex untuk memastikan keamanan thread. Gunakan defer untuk membuka kunci. Biarkan mutex dan semua field tidak diekspor (huruf kecil) agar kode eksternal harus menggunakan method-mu.

  • main.go: Proses operasi perbankan dan tunjukkan penggunaan rekening yang aman untuk thread.

    Baca saldo awal, lalu jumlah operasi. Untuk setiap operasi, baca jenisnya (deposit, withdraw, atau balance), lalu untuk deposit/withdraw, baca jumlahnya.

    Cetak hasil untuk setiap operasi:

    • deposit: Cetak Deposited [amount], Balance: [new balance]
    • withdraw: Cetak Withdrew [amount], Balance: [new balance] jika berhasil, atau Withdrawal failed: insufficient funds jika tidak
    • balance: Cetak Current balance: [balance]

    Setelah semua operasi, cetak riwayat transaksi dengan setiap entri pada baris baru. Hanya awali entri pertama dengan History: .

Input berikut akan diberikan:

  • Baris 1: Saldo awal (bilangan bulat)
  • Baris 2: Jumlah operasi (bilangan bulat)
  • Baris berikutnya: Untuk setiap operasi, jenisnya (deposit, withdraw, atau balance), lalu untuk deposit/withdraw, jumlahnya pada baris berikutnya

Contohnya, jika diberikan:

100
5
deposit
50
balance
withdraw
30
withdraw
200
balance

Output-mu seharusnya:

Deposited 50, Balance: 150
Current balance: 150
Withdrew 30, Balance: 120
Withdrawal failed: insufficient funds
Current balance: 120
History: +50
-30

Prinsip utamanya adalah semua sinkronisasi disembunyikan di dalam method BankAccount-mu. Pemanggil cukup menggunakan Deposit(), Withdraw(), dan Balance() tanpa perlu memikirkan lock. Struct-mu menangani keamanan thread secara internal.

Coba sendiri

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// Baca saldo awal
	initialStr, _ := reader.ReadString('\n')
	initial, _ := strconv.Atoi(strings.TrimSpace(initialStr))

	// Baca jumlah operasi
	numOpsStr, _ := reader.ReadString('\n')
	numOps, _ := strconv.Atoi(strings.TrimSpace(numOpsStr))

	// Buat rekening bank
	account := NewBankAccount(initial)

	// Proses setiap operasi
	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: Panggil Deposit dan cetak hasilnya
			// Format: "Deposited [amount], Balance: [new balance]"

		case "withdraw":
			amountStr, _ := reader.ReadString('\n')
			amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
			// TODO: Panggil Withdraw dan cetak hasil yang sesuai
			// Jika berhasil: "Withdrew [amount], Balance: [new balance]"
			// Jika gagal: "Withdrawal failed: insufficient funds"
			_ = amount // Hapus baris ini saat kamu mengimplementasikannya

		case "balance":
			// TODO: Panggil Balance dan cetak hasilnya
			// Format: "Current balance: [balance]"
		}
	}

	// TODO: Cetak riwayat transaksi
	// Awali entri pertama dengan "History: "
	// Tampilkan entri berikutnya pada baris baru tanpa awalan
}
quiz iconUji dirimu

Pelajaran ini berisi kuis singkat. Mulai pelajarannya untuk menjawab kuis dan melacak progresmu.

Semua pelajaran di Pemrograman Berorientasi Objek

Latihan sendiri: Compiler Go online