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

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() لعمليات الكتابة. يسمح هذا بالقراءات المتزامنة مع ضمان أمان عمليات الكتابة.

challenge icon

التحدي

سهل

لنقم ببناء نظام مخزون آمن برمجياً (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 stock
    • remove: اطبع 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: قراءة الكمية، إزالة المخزون، طباعة النتيجة المناسبة
			
		}
	}
}
quiz iconاختبر نفسك

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

جميع دروس Object Oriented Programming