sync.Mutex و sync.RWMutex
جزء من قسم Object Oriented Programming في رحلة GO على Coddy — الدرس 63 من 107.
بينما تُعد القنوات (channels) هي الطريقة المفضلة في لغة Go لتنسيق الـ goroutines، فأنت تحتاج أحياناً إلى حماية البيانات المشتركة بشكل مباشر. توفر حزمة sync ما يُعرف بـ mutexes—وهي عبارة عن أقفال تضمن وصول goroutine واحد فقط إلى مورد معين في المرة الواحدة.
يمتلك sync.Mutex طريقتين: Lock() و Unlock(). عندما يستدعي goroutine الدالة Lock()، فإنه يحصل على وصول حصري. ستنتظر الـ goroutines الأخرى التي تستدعي Lock() حتى يتم استدعاء Unlock():
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}يضمن استخدام defer c.mu.Unlock() تحرير القفل حتى لو انتهت الدالة مبكرًا أو حدث panic—وهو نمط شائع وموصى به.
عندما تكون عمليات القراءة متكررة ولكن عمليات الكتابة نادرة، يوفر sync.RWMutex أداءً أفضل. فهو يسمح بعدة قراء في نفس الوقت، ولكن يحصل الكتاب على وصول حصري:
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock() // يُسمح بعدة قراء
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // وصول حصري للكتابة
defer c.mu.Unlock()
c.data[key] = value
}استخدم RLock()/RUnlock() لعمليات القراءة و Lock()/Unlock() لعمليات الكتابة. يسمح هذا بالقراءات المتزامنة مع ضمان أمان عمليات الكتابة.
التحدي
سهللنقم ببناء نظام مخزون آمن برمجياً (thread-safe) يتتبع مستويات مخزون المنتجات. سيتعامل نظامك مع عمليات القراءة والكتابة المتزامنة بأمان باستخدام الـ mutexes، مما يضمن سلامة البيانات عند حدوث عمليات متعددة في وقت واحد.
ستقوم بتنظيم الكود الخاص بك عبر ملفين:
inventory.go: حدد نظام إدارة المخزون الآمن برمجياً الخاص بك.أنشئ هيكل
Inventoryيقوم بتخزين كميات المنتجات في خريطة (map) ويستخدمsync.RWMutexلحماية الوصول. يجب أن يدعم المخزون الخاص بك هذه العمليات:NewInventory() *Inventory- ينشئ مخزوناً جديداً مع خريطة مهيأة.AddStock(product string, quantity int)- يضيف كمية إلى مخزون المنتج (استخدم قفلاً حصرياً "exclusive lock" لأن هذا يعدل البيانات).GetStock(product string) int- يرجع المخزون الحالي لمنتج ما، أو 0 إذا لم يتم العثور عليه (استخدم قفل قراءة "read lock" لأن هذا يقرأ البيانات فقط).RemoveStock(product string, quantity int) bool- يزيل الكمية من المخزون إذا توفر مخزون كافٍ. يرجعtrueإذا نجحت العملية، وfalseإذا كان المخزون غير كافٍ (استخدم قفلاً حصرياً).
تذكر استخدام
deferلفك القفل لضمان تحرير الأقفال دائماً بشكل صحيح.main.go: قراءة العمليات واستعراض المخزون الآمن برمجياً الخاص بك.اقرأ عدد العمليات، ثم قم بمعالجة كل عملية. كل عملية لها نوع (
add، أوget، أوremove)، واسم منتج، وبالنسبة لعملياتadd/remove، توجد كمية.لكل عملية، اطبع النتيجة:
add: اطبعAdded [quantity] [product]get: اطبع[product]: [stock] in stockremove: اطبعRemoved [quantity] [product]إذا نجحت العملية، أوInsufficient stock for [product]إذا لم تنجح.
سيتم توفير المدخلات التالية:
- السطر 1: عدد العمليات (عدد صحيح)
- الأسطر التالية: لكل عملية:
- نوع العملية (
add، أوget، أوremove) - اسم المنتج
- الكمية (فقط لعمليات
addوremove)
- نوع العملية (
على سبيل المثال، إذا كان المعطى:
5
add
apples
50
get
apples
remove
apples
30
remove
apples
25
get
applesيجب أن يكون مخرجك:
Added 50 apples
apples: 50 in stock
Removed 30 apples
Insufficient stock for apples
apples: 20 in stockيسمح RWMutex لعدة استدعاءات لـ GetStock بالقراءة في وقت واحد، بينما تحصل AddStock و RemoveStock على وصول حصري عند تعديل المخزون.
ورقة مرجعية
توفر حزمة sync **أقفال الاستبعاد المتبادل (mutexes)** لحماية البيانات المشتركة عندما تحتاج عدة goroutines إلى وصول مباشر إلى نفس المورد.
sync.Mutex
يوفر sync.Mutex وصولاً حصرياً باستخدام Lock() و Unlock():
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}يضمن استخدام defer مع Unlock() تحرير القفل حتى لو انتهت الدالة مبكراً أو حدث panic.
sync.RWMutex
يسمح sync.RWMutex بعدة قراء متزامنين ولكن وصول حصري للكتاب:
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock() // يسمح بعدة قراء
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // وصول حصري للكتابة
defer c.mu.Unlock()
c.data[key] = value
}استخدم RLock()/RUnlock() لعمليات القراءة و Lock()/Unlock() لعمليات الكتابة.
جرّب بنفسك
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// قراءة عدد العمليات
line, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(line))
// إنشاء مخزون جديد
inventory := NewInventory()
// معالجة كل عملية
for i := 0; i < numOps; i++ {
// قراءة نوع العملية
opLine, _ := reader.ReadString('\n')
opType := strings.TrimSpace(opLine)
// قراءة اسم المنتج
productLine, _ := reader.ReadString('\n')
product := strings.TrimSpace(productLine)
// TODO: التعامل مع كل نوع عملية (إضافة، جلب، إزالة)
// بالنسبة لـ "add" و "remove"، اقرأ الكمية من المدخلات
// استدعاء طريقة المخزون المناسبة
// طباعة النتيجة وفقاً لوصف التحدي
switch opType {
case "add":
// TODO: قراءة الكمية، إضافة المخزون، طباعة النتيجة
case "get":
// TODO: جلب المخزون، طباعة النتيجة
case "remove":
// TODO: قراءة الكمية، إزالة المخزون، طباعة النتيجة المناسبة
}
}
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس 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