Skip to content

cond

sync.Cond هو متغير الشرط في مكتبة Go القياسية، وهو الأداة الوحيدة للمزامنة التي تحتاج تهيئة يدوية. على عكس بدائل المزامنة الأخرى، sync.Cond يحتاج تمرير قفل مزامنة (sync.Mutex) لحماية الوصول للموارد المشتركة. يسمح للكوروتين بالدخول في حالة انتظار حتى يتحقق شرط معين، ثم يُوقظ عند تحققه.

مثال على الكود

go
package main

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

var i = 0

func main() {
    var mu sync.Mutex
    var wg sync.WaitGroup

    // إنشاء متغير شرط وتمرير قفل المزامنة
    cd := sync.NewCond(&mu)

    // إضافة 4 كوروتينات معلقة
    wg.Add(4)

    // إنشاء 3 كوروتينات، كل كوروتين سينتظر تحقق الشرط
   	for j := range 3 {
		go func() {
			defer wg.Done()

			mu.Lock()
			for i <= 100 {
                 // عندما لا يتحقق الشرط، الكوروتين يُحجب هنا
				cd.Wait()
			}
			fmt.Printf("%d wake up\n", j)
			mu.Unlock()
		}()
	}

    // إنشاء كوروتين لتحديث الشرط وإيقاظ الكوروتينات الأخرى
    go func() {
        defer wg.Done()
        for {
            mu.Lock()
            i++ // تحديث المتغير المشترك
            mu.Unlock()
            if i > 100 {
                cd.Broadcast() // إيقاظ جميع الكوروتينات المنتظرة عند تحقق الشرط
                break
            }
            time.Sleep(time.Millisecond * 10) // محاكاة عبء العمل
        }
    }()

    // انتظار اكتمال جميع الكوروتينات
    wg.Wait()
}

في المثال أعلاه، المتغير المشترك i يُوصل ويُعدل من عدة كوروتينات. عبر قفل المزامنة mu نضمن أن عمليات الوصول لـ i آمنة في ظل التزامن. ثم، عبر sync.NewCond(&mu) أُنشئ متغير شرط cd، يعتمد على القفل mu لضمان مزامنة الوصول للموارد المشتركة عند الانتظار.

  • ثلاثة كوروتينات منتظرة: كل كوروتين يحجب نفسه عبر cd.Wait() حتى يتحقق الشرط (i > 100). هذه الكوروتينات تبقى في حالة حجب حتى تُحدّث قيمة المتغير المشترك i.
  • كوروتين يُحدّث الشرط ويُوقظ الآخرين: عندما يتحقق الشرط (أي i > 100)، هذا الكوروتين يُوقظ جميع الكوروتينات المنتظرة عبر cd.Broadcast()، ليتابعوا التنفيذ.

البنية

go
type Cond struct {
	// L is held while observing or changing the condition
	L Locker

	notify  notifyList
}

type notifyList struct {
	// wait is the ticket number of the next waiter. It is atomically
	// incremented outside the lock.
	wait atomic.Uint32

	notify uint32

	// List of parked waiters.
	lock mutex
	head *sudog
	tail *sudog
}

بنيته ليست معقدة:

  • L، قفل المزامنة، النوع هنا هو واجهة Locker وليس نوع قفل محدد
  • notify، قائمة إشعارات الكوروتينات المنتظرة

الأهم هو بنية runtime.notifyList:

  • wait، قيمة ذرية، تسجل عدد الكوروتينات المنتظرة
  • notify، تشير للكوروتين التالي الذي سيُوقظ، تبدأ من 0 وتتزايد
  • lock، قفل مزامنة، ليس القفل الذي مررناه، بل قفل داخلي في runtime
  • head، tail، مؤشرات القائمة

له ثلاث طرق إجمالًا:

  • Wait، حجب وانتظار
  • Signal، إيقاظ كوروتين منتظر واحد
  • Broadcast، إيقاظ جميع الكوروتينات المنتظرة

معظم تنفيذه مخفي في مكتبة runtime، هذا التنفيذ موجود في ملف runtime/sema.go، لذلك الكود في المكتبة القياسية قصير جدًا، ومبدأه الأساسي هو طابور حجب مع قفل.

Wait

طريقة Wait تجعل الكوروتين يدخل في حالة حجب وانتظار حتى يُوقظ.

go
func (c *Cond) Wait() {
    t := runtime_notifyListAdd(&c.notify)
    c.L.Unlock()
    runtime_notifyListWait(&c.notify, t)
    c.L.Lock()
}

أولًا تضيف نفسها لـ notifyList، لكن في الواقع فقط تزيد notifyList.wait بواحد، هذه العملية تعادل len(notifyList)-1، تحصل على فهرس آخر عنصر.

go
func notifyListAdd(l *notifyList) uint32 {
	return l.wait.Add(1) - 1
}

عملية الإضافة الحقيقية تتم في دالة notifyListWait.

go
func notifyListWait(l *notifyList, t uint32) {
	...
}

في هذه الدالة، أولًا تُقفل القائمة، ثم تتحقق بسرعة هل الكوروتين الحالي قد أُوقظ بالفعل، إذا أُوقظ تعود مباشرة بدون حاجة للحجب.

go
lockWithRank(&l.lock, lockRankNotifyList)
// Return right away if this ticket has already been notified.
if less(t, l.notify) {
	unlock(&l.lock)
	return
}

إذا لم يُوقظ، تُبنى بنية sudog وتُضاف للطابور، ثم تُعلق عبر gopark.

go
s := acquireSudog()
s.g = getg()
s.ticket = t
s.releasetime = 0
if l.tail == nil {
	l.head = s
} else {
	l.tail.next = s
}
l.tail = s
goparkunlock(&l.lock, waitReasonSyncCondWait, traceBlockCondWait, 3)

بعد الإيقاظ تُحرر بنية sudog.

go
releaseSudog(s)

Signal

Signal تُوقظ الكوروتين المحجوب حسب ترتيب الوصول أولًا يُخرج أولًا.

go
func (c *Cond) Signal() {
	runtime_notifyListNotifyOne(&c.notify)
}

عمليتها كالتالي:

  1. بدون قفل تتحقق مباشرة، هل l.wait يساوي l.notify، إذا تساويا يعني أن جميع الكوروتينات أُوقظت

    go
    if l.wait.Load() == atomic.Load(&l.notify) {
    	return
    }
  2. بعد القفل، تتحقق مرة أخرى هل جميعها أُوقظت

    go
    lockWithRank(&l.lock, lockRankNotifyList)
    t := l.notify
    if t == l.wait.Load() {
    	unlock(&l.lock)
    	return
    }
  3. زيادة l.notify بواحد

    go
    atomic.Store(&l.notify, t+1)
  4. تعبر القائمة دوريًا، تجد الكوروتين الذي يجب إيقاظه، وأخيرًا عبر runtime.goready تُوقظ الكوروتين.

    go
    for p, s := (*sudog)(nil), l.head; s != nil; p, s = s, s.next {
    	if s.ticket == t {
    		n := s.next
    		if p != nil {
    			p.next = n
    		} else {
    			l.head = n
    		}
    		if n == nil {
    			l.tail = p
    		}
    		unlock(&l.lock)
    		s.next = nil
    		readyWithTime(s, 4)
    		return
    	}
    }
    unlock(&l.lock)

Broadcast

Broadcast تُوقظ جميع الكوروتينات المحجوبة.

go
func (c *Cond) Broadcast() {
    runtime_notifyListNotifyAll(&c.notify)
}

عمليتها متشابهة أساسًا:

  1. فحص بدون قفل، هل جميعها أُوقظت

    go
    // Fast-path: if there are no new waiters since the last notification
    // we don't need to acquire the lock.
    if l.wait.Load() == atomic.Load(&l.notify) {
    	return
    }
  2. قفل، إفراغ القائمة، ثم تحرير القفل، الكوروتينات الجديدة اللاحقة ستُضاف في رأس القائمة

    go
    lockWithRank(&l.lock, lockRankNotifyList)
    s := l.head
    l.head = nil
    l.tail = nil
    atomic.Store(&l.notify, l.wait.Load())
    unlock(&l.lock)
  3. عبور القائمة، إيقاظ جميع الكوروتينات

    go
    for s != nil {
    	next := s.next
    	s.next = nil
    	readyWithTime(s, 4)
    	s = next
    }

ملخص

sync.Cond أكثر سيناريوهات استخدامًا شيوعًا هي الحاجة للمزامنة بين عدة كوروتينات حول شروط معينة، تُطبق عادةً في نموذج المنتج-المستهلك، وجدولة المهام، وغيرها. في هذه السيناريوهات، عدة كوروتينات تحتاج لانتظار تحقق شروط معينة لمتابعة التنفيذ، أو تحتاج للإشعار عند تغير الشروط. يوفر طريقة مرنة وفعالة لإدارة المزامنة بين الكوروتينات. بالاستخدام مع قفل المزامنة، sync.Cond يضمن أمان الوصول للموارد المشتركة، ويمكنه التحكم في ترتيب تنفيذ الكوروتينات عند تحقق شروط محددة. فهم مبادئ تنفيذه الداخلية يساعدنا على إتقان تقنيات البرمجة المتزامنة بشكل أفضل، خاصة عند التعامل مع مزامنة الشروط المعقدة.

Golang تم تحريره بواسطة www.golangdev.cn