Menu
Coddy logo textTech

תכנון Struct בטוח לשימוש במקביל

חלק מהיחידה תכנות מונחה עצמים במסלול ה-GO של Coddy. שיעור 65 מתוך 107.

עכשיו, כשאתה מבין mutexes ו־WaitGroups, בוא נשלב אותם כדי לתכנן מבנים שבטוח להשתמש בהם מכמה goroutines בו־זמנית. מבנה בטוח לשימוש במקביל עוטף את מנגנוני הסנכרון בתוך המתודות שלו, כך שהקוראים לו לא צריכים לדאוג לנעילה.

התבנית פשוטה: הטמע mutex במבנה שלך ונעל אותו בכל מתודה שניגשת למצב משותף:

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
}

שימו לב שאפילו המתודה לקריאה בלבד Value() נועלת את המיוטקס. בלי זה, גורוטינה אחת עלולה לקרוא בזמן שגורוטינה אחרת כותבת, ולגרום למרוץ נתונים. אם קריאות נפוצות הרבה יותר מכתיבות, השתמשו ב־sync.RWMutex וקראו ל־RLock() עבור קריאות.

עיקרון עיצובי מרכזי: שומרים את המיוטקס פרטי. שימוש בשם שדה באותיות קטנות (mu) מונע מקוד חיצוני לגשת אליו ישירות. כל הסנכרון מתבצע באמצעות המתודות שלך, וכך יש לך שליטה מלאה בבטיחות של עבודה מקבילית.

במבנים עם מספר שדות, הגנו על כל השדות הקשורים באמצעות אותו mutex כדי להבטיח מצב עקבי:

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 וגם history מתעדכנים באופן אטומי: אף גורוטינה לא יכולה לראות מצב לא עקבי שבו אחד מהם השתנה והשני לא.

challenge icon

אתגר

קל

בואו נבנה מערכת של חשבון בנק בטוחה לשימוש במקביל, שמדגימה הסתרה נכונה של סנכרון בתוך המתודות של מבנה. החשבון יטפל בבטחה בהפקדות, במשיכות ובבדיקות יתרה במקביל, בלי לחשוף למי שקוראים לו פרטים כלשהם על נעילות.

תארגנו את הקוד שלכם בשני קבצים:

  • account.go: הגדירו את חשבון הבנק הבטוח לשימוש במקביל שלכם.

    צרו מבנה BankAccount שכולל sync.Mutex מוטמע, שדה balance (מסוג int) ופרוסת transactions שמתעדת את כל הפעולות שהצליחו כמחרוזות.

    ממשו את המתודות האלה:

    • NewBankAccount(initial int) *BankAccount - יוצרת חשבון חדש עם היתרה ההתחלתית הנתונה ופרוסת עסקאות ריקה
    • Deposit(amount int) - מוסיפה את הסכום ליתרה ומתעדת את העסקה בתור +[amount]
    • Withdraw(amount int) bool - אם יש מספיק כסף, מחסרת את הסכום, מתעדת -[amount] ומחזירה true. אחרת, מחזירה false בלי לשנות דבר
    • Balance() int - מחזירה את היתרה הנוכחית
    • History() []string - מחזירה עותק של פרוסת העסקאות

    כל מתודה שניגשת לשדות של המבנה חייבת לנעול את המנעול כדי להבטיח בטיחות בשימוש במקביל. השתמשו ב-defer לשחרור הנעילה. השאירו את המנעול ואת כל השדות לא מיוצאים (באותיות קטנות), כדי שקוד חיצוני יהיה חייב להשתמש במתודות שלכם.

  • main.go: עבדו את פעולות הבנק והדגימו את החשבון הבטוח לשימוש במקביל שלכם.

    קראו את היתרה ההתחלתית, ולאחר מכן את מספר הפעולות. עבור כל פעולה, קראו את הסוג (deposit, withdraw או balance), ובהפקדה או במשיכה קראו גם את הסכום.

    הדפיסו את התוצאות של כל פעולה:

    • deposit: הדפיסו Deposited [amount], Balance: [new balance]
    • withdraw: אם הפעולה הצליחה, הדפיסו Withdrew [amount], Balance: [new balance], ואם לא, הדפיסו Withdrawal failed: insufficient funds
    • balance: הדפיסו Current balance: [balance]

    לאחר כל הפעולות, הדפיסו את היסטוריית העסקאות, כשכל רשומה מופיעה בשורה חדשה, ורק לפני הרשומה הראשונה מופיעה הקידומת History: .

הקלטים הבאים יסופקו:

  • שורה 1: יתרה התחלתית (מספר שלם)
  • שורה 2: מספר הפעולות (מספר שלם)
  • השורות הבאות: עבור כל פעולה, הסוג (deposit, withdraw או balance), ובהפקדה או במשיכה, הסכום בשורה הבאה

לדוגמה, בהינתן:

100
5
deposit
50
balance
withdraw
30
withdraw
200
balance

הפלט שלכם אמור להיות:

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

העיקרון המרכזי כאן הוא שכל הסנכרון מוסתר בתוך המתודות של BankAccount שלכם. מי שקוראים למתודות פשוט משתמשים ב-Deposit(), ב-Withdraw() וב-Balance(), בלי לחשוב בכלל על נעילות. המבנה שלכם מטפל בבטיחות בשימוש במקביל באופן פנימי.

נסו בעצמכם

package main

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

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

	// קרא את היתרה ההתחלתית
	initialStr, _ := reader.ReadString('\n')
	initial, _ := strconv.Atoi(strings.TrimSpace(initialStr))

	// קרא את מספר הפעולות
	numOpsStr, _ := reader.ReadString('\n')
	numOps, _ := strconv.Atoi(strings.TrimSpace(numOpsStr))

	// צור את חשבון הבנק
	account := NewBankAccount(initial)

	// עבד כל פעולה
	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 והדפס את התוצאה
			// תבנית: "Deposited [amount], Balance: [new balance]"

		case "withdraw":
			amountStr, _ := reader.ReadString('\n')
			amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
			// TODO: קרא ל-Withdraw והדפס את התוצאה המתאימה
			// אם הצליח: "Withdrew [amount], Balance: [new balance]"
			// אם נכשל: "Withdrawal failed: insufficient funds"
			_ = amount // הסר שורה זו בעת המימוש

		case "balance":
			// TODO: קרא ל-Balance והדפס את התוצאה
			// תבנית: "Current balance: [balance]"
		}
	}

	// TODO: הדפס את היסטוריית העסקאות
	// לרשומה הראשונה יש להוסיף את הקידומת "History: "
	// יש להציג את הרשומות הבאות בשורות חדשות, ללא קידומת
}
quiz iconבחנו את עצמכם

השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.

כל השיעורים ביחידה תכנות מונחה עצמים

תרגלו בעצמכם: קומפיילר Go אונליין