حزمة sync

الدرس 16: حزمة sync — حارس سلامة التزامن

تشبيه

تخيل مرحاضًا عامًا:


المفاهيم الأساسية

1. لماذا نحتاج حزمة sync؟

goroutines في Go خفيفة وقوية، لكن عندما تصل عدة goroutines إلى بيانات مشتركة في نفس الوقت، تحدث حالات التسابق:

GO
// خطير! عدة goroutines تعدّل count في نفس الوقت
var count int
for i := 0; i < 1000; i++ {
    go func() {
        count++ // تسابق بيانات!
    }()
}

توفر حزمة sync أدوات المزامنة لضمان سلامة التزامن.

2. نظرة عامة على الأنواع الأساسية

النوع الغرض الخصائص
sync.Mutex قفل إقصاء متبادل goroutine واحد فقط يمكنه احتلاله في كل مرة
sync.RWMutex قفل قراءة-كتابة عدة قراء، كاتب واحد؛ القراءات لا تتعارض
sync.WaitGroup مجموعة انتظار انتظار مجموعة goroutines للانتهاء (غُطيت في الدرس 15)
sync.Once تنفيذ واحد يضمن تنفيذ الدالة مرة واحدة فقط
sync.Pool تجمع objects إعادة استخدام objects، تقليل تخصيصات الذاكرة
sync.Map خريطة متزامنة تخزين مفاتيح-قيم متزامن بدون قفل
sync.Cond متغير شرط إخطار شرطي بين goroutines
حزمة atomic عمليات ذرية عمليات سلامة التزامن الأقل مستوى والأكثر كفاءة

البنية الأساسية والاستخدام

💡 Mutex

GO
package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex
    count := 0
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.Lock()         // قفل
            count++           // تعديل آمن للمتغير المشترك
            mu.Unlock()       // فتح
        }()
    }

    wg.Wait()
    fmt.Println("count =", count) // الإخراج: count = 1000
}
💡 نصيحة: استخدام defer mu.Unlock() يضمن تحرير القفل حتى لو حدث panic في الدالة، مما يمنع الجمود.

💡 RWMutex (قفل قراءة-كتابة)

GO
package main

import (
    "fmt"
    "sync"
)

func main() {
    var rwmu sync.RWMutex
    data := make(map[string]string)
    var wg sync.WaitGroup

    // عملية كتابة: تستخدم قفل كتابة
    wg.Add(1)
    go func() {
        defer wg.Done()
        rwmu.Lock() // قفل كتابة: حصري
        data["key"] = "value"
        rwmu.Unlock()
    }()

    // عملية قراءة: تستخدم قفل قراءة (يمكن أن تكون متزامنة)
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            rwmu.RLock() // قفل قراءة: مشترك
            _ = data["key"]
            rwmu.RUnlock()
        }()
    }

    wg.Wait()
    fmt.Println("data:", data)
}
💡 نصيحة: للسيناريوهات كثيرة القراءة وقليلة الكتابة، RWMutex أفضل أداءً؛ إذا كانت ترددات القراءة والكتابة متشابهة، Mutex أبسط.

💡 Once (تنفيذ واحد)

GO
package main

import (
    "fmt"
    "sync"
)

func main() {
    var once sync.Once
    var wg sync.WaitGroup

    setup := func() {
        fmt.Println("التهيئة (تعمل مرة واحدة فقط)")
    }

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            once.Do(setup) // فقط الـ goroutine الأول سينفذ setup
            fmt.Printf("goroutine %d اكتمل\n", id)
        }(i)
    }

    wg.Wait()
}
💡 نصيحة: يضمن Once أنه حتى لو استدعته عدة goroutines في نفس الوقت، الدالة المقدمة ستنفذ مرة واحدة فقط. يُستخدم عادةً لتهيئة المُفرد (Singleton).

💡 Pool (تجمع objects)

GO
package main

import (
    "fmt"
    "sync"
)

func main() {
    pool := &sync.Pool{
        New: func() interface{} {
            fmt.Println("إنشاء object جديد")
            return make([]byte, 1024)
        },
    }

    // Get الأول: يستدعي New للإنشاء
    buf1 := pool.Get().([]byte)
    fmt.Println("تم الحصول على object، الطول:", len(buf1))

    // إعادة بعد الاستخدام
    pool.Put(buf1)

    // Get الثاني: يعيد استخدام object المُعاد سابقًا
    buf2 := pool.Get().([]byte)
    fmt.Println("تم إعادة استخدام object، الطول:", len(buf2))
}
💡 نصيحة: objects في Pool قد تُجمع كقمامة أثناء أي دورة GC. لا تفترض أن object مُعاد عبر Put يمكن دائمًا استرداده عبر Get.

💡 العمليات الذرية

GO
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    var count int64 = 0
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            atomic.AddInt64(&count, 1) // زيادة ذرية
        }()
    }

    wg.Wait()
    fmt.Println("count =", atomic.LoadInt64(&count)) // قراءة ذرية
}
💡 نصيحة: عمليات atomic أخف من Mutex، مناسبة للعدادات البسيطة، الأعلام، إلخ.

💡 اكتشاف التسابق

BASH
go run -race main.go    # اكتشاف التسابق وقت التشغيل
go test -race ./...     # اكتشاف التسابق أثناء الاختبار
💡 نصيحة: يُوصى بتمكين -race دائمًا أثناء التطوير؛ يساعدك على اكتشاف مشكلات التسابق الخفية.


أمثلة عملية

مثال: عداد آمن (الصعوبة ⭐)

عداد آمن مُغلَّف بقفل:

GO
package main

import (
    "fmt"
    "sync"
)

// SafeCounter عداد آمن للتزامن
type SafeCounter struct {
    mu    sync.Mutex
    count int
}

// Inc يزيد العداد
func (c *SafeCounter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

// Value يُعيد القيمة الحالية
func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

func main() {
    counter := &SafeCounter{}
    var wg sync.WaitGroup

    // بدء 100 goroutine، كل واحدة تزيد 100 مرة
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for j := 0; j < 100; j++ {
                counter.Inc()
            }
        }()
    }

    wg.Wait()
    fmt.Println("العدد النهائي:", counter.Value()) // الإخراج: العدد النهائي: 10000
}
▶ جرّب الكود

مثال: ذاكرة مؤقتة متزامنة مع sync.Map (الصعوبة ⭐⭐)

GO
package main

import (
    "fmt"
    "sync"
)

// Cache ذاكرة مؤقتة متزامنة
type Cache struct {
    store sync.Map
}

// Set تخزين قيمة في الذاكرة المؤقتة
func (c *Cache) Set(key string, value interface{}) {
    c.store.Store(key, value)
}

// Get استرجاع قيمة من الذاكرة المؤقتة
func (c *Cache) Get(key string) (interface{}, bool) {
    return c.store.Load(key)
}

// Delete حذف مفتاح من الذاكرة المؤقتة
func (c *Cache) Delete(key string) {
    c.store.Delete(key)
}

// GetOrSet استرجاع أو تعيين قيمة بشكل ذري (تجنب الحساب المكرر)
func (c *Cache) GetOrSet(key string, factory func() interface{}) interface{} {
    if val, ok := c.store.Load(key); ok {
        return val
    }
    // استخدام LoadOrStore لضمان الذرية
    val, _ := c.store.LoadOrStore(key, factory())
    return val
}

func main() {
    cache := &Cache{}
    var wg sync.WaitGroup

    // كتابة متزامنة
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            key := fmt.Sprintf("key-%d", id)
            cache.Set(key, id*10)
            fmt.Printf("كتابة: %s = %d\n", key, id*10)
        }(i)
    }

    wg.Wait()

    // قراءة متزامنة
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            key := fmt.Sprintf("key-%d", id)
            if val, ok := cache.Get(key); ok {
                fmt.Printf("قراءة: %s = %v\n", key, val)
            }
        }(i)
    }

    wg.Wait()

    // استخدام GetOrSet لتجنب الحساب المكرر
    result := cache.GetOrSet("computed", func() interface{} {
        fmt.Println("تنفيذ حساب معقد...")
        return 42
    })
    fmt.Println("computed =", result)

    // Get مرة أخرى، لن يُعاد الحساب
    result2 := cache.GetOrSet("computed", func() interface{} {
        fmt.Println("هذا السطر لن يُنفذ")
        return 99
    })
    fmt.Println("computed =", result2)
}
▶ جرّب الكود

مثال: تجمع عمال مع مهلة (استخدام شامل لـ sync) (الصعوبة ⭐⭐⭐)

GO
package main

import (
    "context"
    "fmt"
    "math/rand"
    "sync"
    "sync/atomic"
    "time"
)

// Task تمثل مهمة
type Task struct {
    ID   int
    Data string
}

// Result تمثل نتيجة
type Result struct {
    TaskID   int
    Output   string
    WorkerID int
    Duration time.Duration
}

// WorkerPool تجمع عمال
type WorkerPool struct {
    workerCount int
    taskCh      chan Task
    resultCh    chan Result
    wg          sync.WaitGroup
    processed   int64 // عداد ذري
    errors      int64
    once        sync.Once // يضمن التهيئة مرة واحدة
    pool        sync.Pool // إعادة استخدام objects النتيجة
}

// NewWorkerPool إنشاء تجمع عمال
func NewWorkerPool(workerCount, taskBufferSize int) *WorkerPool {
    wp := &WorkerPool{
        workerCount: workerCount,
        taskCh:      make(chan Task, taskBufferSize),
        resultCh:    make(chan Result, taskBufferSize),
    }

    // تهيئة تجمع objects
    wp.pool = sync.Pool{
        New: func() interface{} {
            return &Result{}
        },
    }

    return wp
}

// Start بدء تجمع العمال (ينفذ مرة واحدة فقط)
func (wp *WorkerPool) Start(ctx context.Context) {
    wp.once.Do(func() {
        for i := 0; i < wp.workerCount; i++ {
            wp.wg.Add(1)
            go wp.worker(ctx, i)
        }
        fmt.Printf("بدأ تجمع العمال بـ %d عمال\n", wp.workerCount)
    })
}

// worker goroutine العامل
func (wp *WorkerPool) worker(ctx context.Context, id int) {
    defer wp.wg.Done()

    for {
        select {
        case <-ctx.Done():
            fmt.Printf("العامل %d: استقبل إشارة الخروج\n", id)
            return
        case task, ok := <-wp.taskCh:
            if !ok {
                fmt.Printf("العامل %d: قناة المهام أُغلقت\n", id)
                return
            }
            // الحصول على Result من التجمع
            result := wp.pool.Get().(*Result)
            result.TaskID = task.ID
            result.WorkerID = id

            // محاكاة معالجة المهمة
            start := time.Now()
            time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
            result.Output = fmt.Sprintf("تمت المعالجة: %s", task.Data)
            result.Duration = time.Since(start)

            atomic.AddInt64(&wp.processed, 1)
            wp.resultCh <- *result

            // إعادة object Result إلى التجمع (ملاحظة: يُرسل نسخة قيمة)
            wp.pool.Put(result)
        }
    }
}

// Submit تقديم مهمة
func (wp *WorkerPool) Submit(task Task) {
    wp.taskCh <- task
}

// Close إغلاق تجمع العمال
func (wp *WorkerPool) Close() {
    close(wp.taskCh)
    wp.wg.Wait()
    close(wp.resultCh)
}

// Stats إرجاع الإحصائيات
func (wp *WorkerPool) Stats() (processed, errors int64) {
    return atomic.LoadInt64(&wp.processed), atomic.LoadInt64(&wp.errors)
}

func main() {
    rand.Seed(time.Now().UnixNano())

    // إنشاء سياق مع مهلة
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // إنشاء تجمع عمال
    pool := NewWorkerPool(3, 20)
    pool.Start(ctx)

    // تقديم المهام
    var submitWg sync.WaitGroup
    submitWg.Add(1)
    go func() {
        defer submitWg.Done()
        for i := 1; i <= 10; i++ {
            pool.Submit(Task{
                ID:   i,
                Data: fmt.Sprintf("بيانات-المهمة-%d", i),
            })
            fmt.Printf("تم تقديم المهمة #%d\n", i)
        }
    }()

    // جمع النتائج
    var collectWg sync.WaitGroup
    collectWg.Add(1)
    go func() {
        defer collectWg.Done()
        for result := range pool.resultCh {
            fmt.Printf("المهمة#%d تمت معالجتها بواسطة العامل%d، استغرقت %v -> %s\n",
                result.TaskID, result.WorkerID, result.Duration, result.Output)
        }
    }()

    // انتظار اكتمال تقديم المهام
    submitWg.Wait()

    // إغلاق تجمع العمال
    pool.Close()

    // انتظار اكتمال جمع النتائج
    collectWg.Wait()

    // طباعة الإحصائيات
    processed, _ := pool.Stats()
    fmt.Printf("\nالإحصائيات: تمت معالجة %d مهمة إجمالًا\n", processed)
}
▶ جرّب الكود

حالات الاستخدام العملية

الحالة 1: تحديد المعدل لطلبات HTTP المتزامنة

في الزاحفات أو استدعاءات API، تحتاج لتحديد التزامن لتجنب الحظر:

GO
package main

import (
    "fmt"
    "sync"
    "time"
)

// RateLimiter محدد معدل متزامن بسيط
type RateLimiter struct {
    sem     chan struct{}
    mu      sync.Mutex
    active  int
    maxConc int
}

// NewRateLimiter إنشاء محدد معدل؛ maxConc هو الحد الأقصى للتزامن
func NewRateLimiter(maxConc int) *RateLimiter {
    return &RateLimiter{
        sem:     make(chan struct{}, maxConc),
        maxConc: maxConc,
    }
}

// Acquire الحصول على تصريح
func (r *RateLimiter) Acquire() {
    r.sem <- struct{}{}
    r.mu.Lock()
    r.active++
    r.mu.Unlock()
}

// Release تحرير تصريح
func (r *RateLimiter) Release() {
    <-r.sem
    r.mu.Lock()
    r.active--
    r.mu.Unlock()
}

// ActiveCount إرجاع العدد النشط الحالي
func (r *RateLimiter) ActiveCount() int {
    r.mu.Lock()
    defer r.mu.Unlock()
    return r.active
}

func main() {
    limiter := NewRateLimiter(3) // أقصى 3 متزامنة
    var wg sync.WaitGroup

    urls := []string{
        "https://api.example.com/page1",
        "https://api.example.com/page2",
        "https://api.example.com/page3",
        "https://api.example.com/page4",
        "https://api.example.com/page5",
        "https://api.example.com/page6",
    }

    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()

            limiter.Acquire()        // الحصول على تصريح (يحظر عند أكثر من 3)
            defer limiter.Release()  // تحرير تصريح

            fmt.Printf("[المتزامنة:%d] بدء الطلب: %s\n", limiter.ActiveCount(), u)
            time.Sleep(time.Duration(100+len(u)*10) * time.Millisecond) // محاكاة الطلب
            fmt.Printf("[المتزامنة:%d] اكتمل الطلب: %s\n", limiter.ActiveCount(), u)
        }(url)
    }

    wg.Wait()
    fmt.Println("اكتملت جميع الطلبات")
}

الحالة 2: تحميل التكوين الكسول (نمط المُفرد)

استخدم sync.Once لضمان تحميل التكوين مرة واحدة فقط:

GO
package main

import (
    "fmt"
    "sync"
)

// Config تكوين التطبيق
type Config struct {
    DatabaseURL string
    APIKey      string
    MaxRetries  int
}

var (
    config *Config
    once   sync.Once
)

// GetConfig الحصول على التكوين (تحميل كسول، يُهيأ مرة واحدة فقط)
func GetConfig() *Config {
    once.Do(func() {
        fmt.Println("تحميل التكوين (يعمل مرة واحدة فقط)...")
        // محاكاة تحميل التكوين من ملف أو متغيرات بيئية
        config = &Config{
            DatabaseURL: "postgres://localhost:5432/mydb",
            APIKey:      "sk-xxxxxxxxxxxx",
            MaxRetries:  3,
        }
    })
    return config
}

func main() {
    var wg sync.WaitGroup

    // عدة goroutines تحصل على التكوين في نفس الوقت
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            cfg := GetConfig()
            fmt.Printf("Goroutine %d: DB=%s, Retries=%d\n",
                id, cfg.DatabaseURL, cfg.MaxRetries)
        }(i)
    }

    wg.Wait()
    fmt.Println("اكتمل تحميل التكوين")
}

❓ أسئلة شائعة

س1: كيف تختار بين Mutex و RWMutex؟

السيناريو التوصية
كثيرة القراءة وقليلة الكتابة (مثل ذاكرة مؤقتة) RWMutex — عدة قراءات يمكن أن تكون متزامنة
قراءة/كتابة متوازنة أو كثيرة الكتابة Mutex — أبسط، أقل حملًا
عداد بسيط atomic — الأخف وزنًا
GO
// ❌ استخدام Mutex لسيناريوهات كثيرة القراءة وقليلة الكتابة — أداء ضعيف
var mu sync.Mutex
mu.Lock()
val := cache[key] // عملية قراءة أيضًا حصرية
mu.Unlock()

// ✅ استخدم RWMutex — عمليات القراءة يمكن أن تكون متزامنة
var rwmu sync.RWMutex
rwmu.RLock()
val := cache[key] // عدة goroutines يمكنها القراءة في نفس الوقت
rwmu.RUnlock()

س2: كيف تختار بين sync.Map و خريطة مقفلة؟

GO
// السيناريو 1: أزواج المفاتيح-قيم ثابتة نسبيًا، كثيرة القراءة وقليلة الكتابة -> sync.Map
var m sync.Map
m.Store("key", "value")
val, _ := m.Load("key")

// السيناريو 2: إضافة/حذف متكرر، تحتاج تكرار -> خريطة مقفلة
type SafeMap struct {
    mu sync.RWMutex
    m  map[string]string
}

// sync.Map مناسب لـ:
// 1. المفاتيح تُكتب مرة واحدة لكن تُقرأ عدة مرات (مثل ذاكرة مؤقتة)
// 2. عدة goroutines تقرأ/تكتب مفاتيح مختلفة (لا تداخل)

س3: متى تُجمع objects في Pool كقمامة؟

objects في Pool قد تُمسح أثناء أي دورة GC. لا تستخدم Pool كتخزين طويل الأمد:

GO
// ❌ استخدام خاطئ: استخدام Pool كذاكرة مؤقتة
pool := &sync.Pool{New: func() interface{} { return expensiveObject() }}
obj := pool.Get()
// ... استخدام
pool.Put(obj)
// بعد GC التالي، ربما جُمع obj

// ✅ استخدام صحيح: إعادة استخدام objects مؤقتة، تقليل التخصيصات
pool := &sync.Pool{
    New: func() interface{} {
        return make([]byte, 0, 4096) // مخزن مُخصص مسبقًا
    },
}
buf := pool.Get().([]byte)[:0] // الحصول وإعادة تعيين
// ... استخدام buf
pool.Put(buf) // إرجاع

س4: كيف تتجنب الجمود؟

GO
// ❌ جمود: نفس الـ goroutine يقفل مرتين
var mu sync.Mutex
mu.Lock()
mu.Lock() // محظور إلى الأبد! جمود!

// ✅ الحل 1: استخدم defer لضمان التحرير
mu.Lock()
defer mu.Unlock()
// ... يُحرر حتى عند panic

// ✅ الحل 2: انتبه لترتيب الأقفال، تجنب الأقفال المتقاطعة
// goroutines: واحد يمسك القفل أ وينتظر ب، والآخر يمسك ب وينتظر أ -> جمود
// الحل: دائمًا أقفل بترتيب أ->ب

// ✅ الحل 3: استخدم أقفالًا مع مهلة (Go 1.18+ يوصى باستخدام context)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// استخدم قناة مع select للتحكم في المهلة

📖 ملخص

المفهوم النقاط الأساسية
sync.Mutex قفل إقصاء متبادل، goroutine واحد فقط يمكنه احتلاله
sync.RWMutex قفل قراءة-كتابة، عدة قراء، كاتب واحد؛ مثالي لسيناريوهات كثيرة القراءة
sync.Once يضمن تنفيذ الدالة مرة واحدة فقط؛ مناسب للمُفردات/التهيئة
sync.Pool تجمع objects، إعادة استخدام objects مؤقتة، تقليل ضغط GC
sync.Map خريطة متزامنة؛ أفضل من خريطة مقفلة في سيناريوهات محددة
atomic عمليات ذرية؛ الحل الأخف لسلامة التزامن
-race كاشف التسابق؛ يجب تمكينه أثناء التطوير
منع الجمود defer Unlock، ترتيب أقفال ثابت، تجنب الأقفال المتداخلة

مبادئ الاختيار:

  1. عداد/علم بسيط → atomic
  2. إقصاء متبادل عام → Mutex
  3. كثيرة القراءة وقليلة الكتابة → RWMutex
  4. تهيئة واحدة → Once
  5. إعادة استخدام objects → Pool
  6. قراءة/كتابة مفاتيح-قيم متزامنة → sync.Map

📝 تمارين

التمرين 1: تنفيذ مكدس آمن

نفّذ مكدسًا متزامنًا (LIFO) يدعم عمليات Push و Pop و Size.

GO
package main

import (
    "errors"
    "fmt"
    "sync"
)

// ThreadSafeStack مكدس متزامن
type ThreadSafeStack struct {
    // الكود الخاص بك:
    // - اختر هيكل بيانات مناسب
    // - اختر أداة مزامنة مناسبة
}

// NewStack إنشاء مكدس جديد
func NewStack() *ThreadSafeStack {
    // الكود الخاص بك
    return nil
}

// Push دفع قيمة onto المكدس
func (s *ThreadSafeStack) Push(val int) {
    // الكود الخاص بك
}

// Pop سحب قيمة من المكدس (يُرجع خطأ عند الفراغ)
func (s *ThreadSafeStack) Pop() (int, error) {
    // الكود الخاص بك
    return 0, errors.New("المكدس فارغ")
}

// Size إرجاع عدد العناصر في المكدس
func (s *ThreadSafeStack) Size() int {
    // الكود الخاص بك
    return 0
}

func main() {
    stack := NewStack()
    var wg sync.WaitGroup

    // دفع متزامن
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(val int) {
            defer wg.Done()
            stack.Push(val)
        }(i)
    }
    wg.Wait()

    fmt.Println("حجم المكدس:", stack.Size()) // يجب أن يطبع 100

    // سحب متزامن
    for i := 0; i < 50; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            if val, err := stack.Pop(); err == nil {
                fmt.Println("سُحب:", val)
            }
        }()
    }
    wg.Wait()

    fmt.Println("الحجم المتبقي:", stack.Size()) // يجب أن يطبع 50
}

التمرين 2: تنفيذ ذاكرة مؤقتة مع TTL

استخدم sync.RWMutex لتنفيذ ذاكرة مؤقتة تدعم TTL (وقت الحياة):

GO
package main

import (
    "fmt"
    "sync"
    "time"
)

// TTLCache ذاكرة مؤقتة مع وقت انتهاء
type TTLCache struct {
    // الكود الخاص بك:
    // - هيكل تخزين
    // - قفل مزامنة
    // - إعداد TTL
}

// NewTTLCache إنشاء ذاكرة مؤقتة؛ ttl هو وقت الانتهاء
func NewTTLCache(ttl time.Duration) *TTLCache {
    // الكود الخاص بك
    return nil
}

// Set تعيين زوج مفتاح-قيمة
func (c *TTLCache) Set(key string, value interface{}) {
    // الكود الخاص بك
}

// Get استرجاع قيمة (يُرجع false إذا منتهية)
func (c *TTLCache) Get(key string) (interface{}, bool) {
    // الكود الخاص بك
    return nil, false
}

// Delete حذف مفتاح
func (c *TTLCache) Delete(key string) {
    // الكود الخاص بك
}

// Size إرجاع عدد أزواج المفاتيح-قيم الصالحة
func (c *TTLCache) Size() int {
    // الكود الخاص بك
    return 0
}

func main() {
    cache := NewTTLCache(2 * time.Second)

    cache.Set("name", "لغة Go")
    cache.Set("version", "1.21")

    if val, ok := cache.Get("name"); ok {
        fmt.Println("name =", val)
    }

    fmt.Println("حجم الذاكرة المؤقتة:", cache.Size())

    time.Sleep(3 * time.Second)

    if _, ok := cache.Get("name"); !ok {
        fmt.Println("name انتهت صلاحيته")
    }

    fmt.Println("الحجم بعد الانتهاء:", cache.Size())
}

التمرين 3: خريطة-تقليص متوازية

استخدم حزمة sync لتنفيذ Map-Reduce متوازي بسيط:

GO
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

// ParallelMapReduce تعالج البيانات بالتوازي وتجمع النتائج
// المعلمات:
//   - data: slice بيانات الإدخال
//   - mapFn: دالة التعيين، تحوّل المدخل إلى قيمة
//   - reduceFn: دالة التقليص، تدمج نتيجتين
//
// المتطلبات:
//   - تقسيم البيانات إلى شُرُع، كل واحدة تُعالج بواسطة goroutine
//   - استخدام sync.WaitGroup لانتظار اكتمال جميع goroutines
//   - استخدام sync.Mutex لحماية نتيجة التقليص
//   - استخدام atomic لعدّاد العناصر المعالجة الإجمالي
func ParallelMapReduce(
    data []int,
    mapFn func(int) int,
    reduceFn func(int, int) int,
) int {
    // الكود الخاص بك:
    // 1. تحديد عدد الشُرُع (يُقترح 4 شُرُع)
    // 2. إطلاق goroutines لمعالجة كل شريحة
    // 3. mapFn تعالج كل عنصر
    // 4. reduceFn تدمج نتائج الشُرُع
    // 5. إرجاع النتيجة النهائية
    return 0
}

func main() {
    data := make([]int, 100)
    for i := range data {
        data[i] = i + 1 // 1 إلى 100
    }

    // حساب مجموع مربعات جميع العناصر
    result := ParallelMapReduce(
        data,
        func(x int) int { return x * x }, // map: مربع
        func(a, b int) int { return a + b }, // reduce: مجموع
    )

    fmt.Println("1² + 2² + 3² + ... + 100² =", result)
    // النتيجة المتوقعة: 338350
}

الدرس التالي

بعد إكمال حزمة sync، لقد أتقنت أدوات Go الأساسية للتزامن. بعد ذلك، سنُعزز ما تعلمناه من خلال تمارين شاملة.

👉 الدرس 17: تمارين التزامن

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%