Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

تصميم الـ Structs الآمنة للخيوط (Thread-Safe)

جزء من قسم البرمجة كائنية التوجه في رحلة GO على Coddy. الدرس 65 من 107.

الآن بعد أن فهمت كائنات mutex ومجموعات WaitGroup، فلنجمع بينها لتصميم هياكل آمنة للاستخدام من عدة 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() تقفل كائن المزامنة أيضًا. من دون ذلك، قد تقرأ إحدى goroutine بينما تكتب أخرى، مما يؤدي إلى حدوث سباق بيانات. إذا كانت عمليات القراءة أكثر شيوعًا بكثير من عمليات الكتابة، فاستخدم sync.RWMutex بدلًا من ذلك واستدعِ RLock() للقراءات.

مبدأ تصميم أساسي: أبقِ كائن المزامنة خاصًا. باستخدام اسم حقل بأحرف صغيرة (mu)، تمنع الشيفرة الخارجية من الوصول إليه مباشرةً. تحدث جميع عمليات المزامنة من خلال أساليبك، مما يمنحك تحكمًا كاملًا في أمان الخيوط.

بالنسبة إلى البنى التي تحتوي على حقول متعددة، احمِ جميع الحقول المرتبطة باستخدام كائن المزامنة نفسه لضمان حالة متسقة:

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 - ينشئ حسابًا جديدًا بالرصيد الأولي المحدد وشريحة transactions فارغة
    • Deposit(amount int) - يضيف المبلغ إلى الرصيد ويسجّل العملية بصيغة +[amount]
    • Withdraw(amount int) bool - إذا كانت الأموال الكافية موجودة، يطرح المبلغ، ويسجّل -[amount]، ويعيد true. وإلا يعيد false من دون تعديل أي شيء
    • Balance() int - يعيد الرصيد الحالي
    • History() []string - يعيد نسخة من شريحة transactions

    يجب على كل أسلوب يصل إلى حقول البنية أن يقفل كائن المزامنة لضمان أمان الخيوط. استخدم 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]"
			// If failed: "Withdrawal failed: insufficient funds"
			_ = amount // أزل هذا السطر عندما تنفّذ

		case "balance":
			// TODO: استدعِ Balance واطبع النتيجة
			// Format: "Current balance: [balance]"
		}
	}

	// TODO: اطبع سجل المعاملات
	// يجب أن تُسبق الإدخالة الأولى بـ "History: "
	// الإدخالات اللاحقة يجب أن تكون في أسطر جديدة بدون بادئة
}
quiz iconاختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

جميع دروس البرمجة كائنية التوجه

تدرّب بنفسك: مترجم Go عبر الإنترنت