تصميم الـ 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 ذريًا: لا يمكن لأي غورووتين ملاحظة حالة غير متسقة تغيّر فيها أحدهما دون الآخر.
التحدي
سهللنبنِ نظام حساب مصرفي آمنًا بالنسبة إلى الخيوط، يوضّح التغليف السليم للمزامنة داخل أساليب البنية. سيتعامل حسابك بأمان مع عمليات الإيداع والسحب والتحقق من الرصيد المتزامنة، من دون كشف أي تفاصيل متعلقة بالقفل للجهات المستدعية.
ستنظّم كودك عبر ملفين:
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: "
// الإدخالات اللاحقة يجب أن تكون في أسطر جديدة بدون بادئة
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس البرمجة كائنية التوجه
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تدرّب بنفسك: مترجم Go عبر الإنترنت