تصميم الـ Structs الآمنة للخيوط (Thread-Safe)
جزء من قسم Object Oriented Programming في رحلة GO على Coddy — الدرس 65 من 107.
الآن بعد أن فهمت الـ mutexes والـ WaitGroups، دعنا نجمع بينهما لتصميم هياكل (structs) آمنة للاستخدام من عدة goroutines في وقت واحد. الـ struct الآمن برمجياً (thread-safe) يغلف التزامن داخل الـ methods الخاصة به، بحيث لا يحتاج المستدعون (callers) للقلق بشأن القفل (locking).
النمط بسيط: قم بتضمين mutex داخل الـ struct الخاص بك وقم بقفله في كل method تصل إلى الحالة المشتركة (shared state):
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() المخصصة للقراءة فقط تقوم بقفل الـ mutex. بدون ذلك، قد تقوم goroutine واحدة بالقراءة بينما تقوم أخرى بالكتابة، مما يتسبب في سباق بيانات (data race). إذا كانت عمليات القراءة أكثر شيوعاً بكثير من عمليات الكتابة، فاستخدم sync.RWMutex بدلاً من ذلك واستدعِ RLock() لعمليات القراءة.
مبدأ تصميم أساسي: اجعل الـ mutex خاصاً. باستخدام اسم حقل يبدأ بحرف صغير (mu)، فإنك تمنع الكود الخارجي من الوصول إليه مباشرة. تتم جميع عمليات المزامنة من خلال دوالك (methods)، مما يمنحك تحكماً كاملاً في سلامة الخيوط (thread safety).
بالنسبة للهياكل (structs) التي تحتوي على حقول متعددة، قم بحماية جميع الحقول ذات الصلة باستخدام نفس الـ 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 بشكل ذري—لا يمكن لأي goroutine ملاحظة حالة غير متسقة حيث يتغير أحدهما دون الآخر.
التحدي
سهللنقم ببناء نظام حساب بنكي آمن برمجياً (thread-safe) يوضح التغليف الصحيح للمزامنة داخل دوال الهيكل (struct methods). سيتعامل حسابك مع عمليات الإيداع، السحب، وفحص الرصيد المتزامنة بأمان دون الكشف عن أي تفاصيل تتعلق بالقفل (locking) للمستدعين.
ستقوم بتنظيم الكود الخاص بك عبر ملفين:
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- يعيد نسخة من شريحة المعاملات
يجب على كل دالة تصل إلى حقول الهيكل أن تقفل الـ mutex لضمان السلامة البرمجية (thread safety). استخدم
deferلفك القفل. اجعل الـ mutex وجميع الحقول غير مصدرة (unexported - تبدأ بحرف صغير) بحيث يجب على الكود الخارجي استخدام دوالك فقط.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() دون التفكير أبداً في الأقفال (locks) - حيث يتعامل الهيكل الخاص بك مع السلامة البرمجية داخلياً.
ورقة مرجعية
تُغلف البنية الآمنة للخيوط (thread-safe struct) التزامن داخل دوالها من خلال تضمين 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
}المبادئ الأساسية:
- قفل الـ mutex في جميع الدوال التي تصل إلى الحالة المشتركة، بما في ذلك دوال القراءة فقط، لمنع سباقات البيانات (data races)
- استخدم
deferلضمان إلغاء قفل الـ mutex حتى لو انتهت الدالة مبكراً - اجعل الـ mutex خاصاً (private) (اسم حقل يبدأ بحرف صغير) حتى لا يتمكن الكود الخارجي من الوصول إليه مباشرة
- بالنسبة لأحمال العمل الكثيفة في القراءة، استخدم
sync.RWMutexواستدعِRLock()للقراءات
بالنسبة للبنيات (structs) التي تحتوي على حقول متعددة، قم بحماية جميع الحقول ذات الصلة باستخدام نفس الـ 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 بشكل ذري (atomically) — بحيث لا يمكن لأي goroutine ملاحظة حالة غير متسقة.
جرّب بنفسك
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: "
// يجب أن تكون المدخلات اللاحقة في أسطر جديدة بدون بادئة
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP في Go
الملفات الخارجيةمساحة العمل والـ Modules في Goالـ Packages والـ Importsالأسماء المصدرة مقابل غير المصدرةمقدمة إلى الـ OOP في Goالـ Structs كـ Classesتعريف الـ Methods في الـ Structsالـ Pointer Receivers مقابل الـ Value Receiversتهيئة الـ Structدوال الـ Constructorمراجعة - آلة حاسبة بسيطة4الواجهات
مقدمة في الواجهاتالتنفيذ الضمنيالواجهة كعقدالواجهة الفارغة (any)تأكيد النوع (Type Assertion)تبديل النوع (Type Switch)تركيب الواجهاتواجهات Stringer و Errorمراجعة - حاسبة الأشكال7التغليف
الحقول المصدرة وغير المصدرةالتغليف على مستوى الحزمةدوال الـ Getter والـ Setterإخفاء المعلومات في Goملخص - سجلات الطلاب10الأنواع العامة (Go 1.18+)
مقدمة في الأنواع العامةمعاملات الأنواعقيود الأنواعالـ Structs العامةحل بديل للـ Methods العامةملخص - المجموعات العامة13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factoryنمط Abstract Factoryنمط Observerنمط Strategy2تعمق في الأنواع و Structs
الأنواع الأساسية والمركبةتعريفات الأنواع المخصصةStruct TagsStructs مجهولةStructs متداخلةالقيم الصفرية والافتراضيةمراجعة - دفتر العناوين5التركيب بدلاً من الوراثة
لماذا لا تدعم Go الوراثةأساسيات تضمين الـ Structترقية الـ Methodتضمين عدة Structsالتضمين مقابل التجميعحجب الـ Methods المضمنةملخص - الهيكل الهرمي للموظفين8معالجة الأخطاء و OOP
واجهة errorأنواع الأخطاء المخصصةتغليف الأخطاء (fmt.Errorf)أخطاء Sentinelerrors.Is() و errors.As()Panic و Defer و Recoverملخص - File Parser11المكتبة القياسية والبرمجة كائنية التوجه (OOP)
io.Reader و io.Writerواجهة sort.Interfaceواجهة fmt.Stringerencoding/json مع Structsواجهة http.Handlerمراجعة - نماذج REST API14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط طريقة القالبنمط الحالةنمط التركيبMiddleware كنمط مزين3المؤشرات والذاكرة
أساسيات المؤشرات في Goالمؤشرات إلى الـ Structsالتمرير بالقيمة مقابل التمرير بالمرجعدالة ()newGarbage Collection في Goملخص - بناء Linked List6تعدد الأشكال في Go
تعدد الأشكال عبر InterfacesDuck Typing في Goقواعد استيفاء الـ Interfaceمجموعات متعددة الأشكالحقن التبعيةمراجعة - معالج الدفع9التزامن والبرمجة كائنية التوجه (OOP)
أساسيات الـ Goroutinesالقنوات (Channels) والاتصالالقنوات المخزنة (Buffered) مقابل غير المخزنةجملة Selectsync.Mutex و sync.RWMutexsync.WaitGroupتصميم الـ Structs الآمنة للخيوط (Thread-Safe)مراجعة - Worker Pool