Go بالأمثلة: الـ Goroutines ذات الحالة (Stateful Goroutines)

في المثال السابق استخدمنا القفل الصريح مع الـ mutexes لتزامن الوصول إلى الحالة المشتركة عبر عدة goroutines. خيار آخر هو استخدام ميزات التزامن المدمجة في الـ goroutines والقنوات لتحقيق نفس النتيجة. يتماشى هذا النهج القائم على القنوات مع أفكار Go في مشاركة الذاكرة عن طريق التواصل وجعل كل قطعة من البيانات مملوكة لـ goroutine واحد بالضبط.

package main

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

في هذا المثال ستكون حالتنا مملوكة لـ goroutine واحد. سيضمن هذا أن البيانات لن تتعرض للتلف أبداً بسبب الوصول المتوازي. لكي تقرأ أو تكتب في تلك الحالة، ستقوم الـ goroutines الأخرى بإرسال رسائل إلى الـ goroutine المالك واستقبال الردود المقابلة. هياكل readOp و writeOp تغلف تلك الطلبات وطريقة للرد من الـ goroutine المالك.

type readOp struct {
    key  int
    resp chan int
}
type writeOp struct {
    key  int
    val  int
    resp chan bool
}

func main() {

كما فعلنا سابقاً، سنعد كم عدد العمليات التي ننفذها.

    var readOps uint64
    var writeOps uint64

سيتم استخدام قناتي reads و writes بواسطة الـ goroutines الأخرى لإصدار طلبات القراءة والكتابة، على التوالي.

    reads := make(chan readOp)
    writes := make(chan writeOp)

إليك الـ goroutine الذي يملك الـ state (الحالة)، وهي خريطة خاصة به. يقوم هذا الـ goroutine مراراً وتكراراً بالاختيار عبر قناتي reads و writes والرد على الطلبات فور وصولها.

    go func() {
        var state = make(map[int]int)
        for {
            select {
            case read := <-reads:
                read.resp <- state[read.key]
            case write := <-writes:
                state[write.key] = write.val
                write.resp <- true
            }
        }
    }()

هذا يبدأ 100 goroutines لإصدار طلبات قراءة للـ goroutine المالك للحالة. تتطلب كل قراءة بناء readOp وإرساله عبر قناة reads ثم استلام النتيجة عبر قناة resp الموفرة.

    for r := 0; r < 100; r++ {
        go func() {
            for {
                read := readOp{
                    key:  rand.Intn(5),
                    resp: make(chan int)}
                reads <- read
                <-read.resp
                atomic.AddUint64(&readOps, 1)
                time.Sleep(time.Millisecond)
            }
        }()
    }

نبدأ 10 عمليات كتابة أيضاً، باستخدام نهج مماثل.

    for w := 0; w < 10; w++ {
        go func() {
            for {
                write := writeOp{
                    key:  rand.Intn(5),
                    val:  rand.Intn(100),
                    resp: make(chan bool)}
                writes <- write
                <-write.resp
                atomic.AddUint64(&writeOps, 1)
                time.Sleep(time.Millisecond)
            }
        }()
    }

دع الـ goroutines تعمل لمدة ثانية واحدة.

    time.Sleep(time.Second)

أخيراً، التقط وابلغ عن أعداد العمليات.

    readOpsFinal := atomic.LoadUint64(&readOps)
    fmt.Println("readOps:", readOpsFinal)
    writeOpsFinal := atomic.LoadUint64(&writeOps)
    fmt.Println("writeOps:", writeOpsFinal)
}

تشغيل البرنامج:

$ go run stateful-goroutines.go
readOps: 71708
writeOps: 7177

المثال التالي: الفرز (Sorting).