cond
sync.Cond هو متغير الشرط في مكتبة Go القياسية، وهو الأداة الوحيدة للمزامنة التي تحتاج تهيئة يدوية. على عكس بدائل المزامنة الأخرى، sync.Cond يحتاج تمرير قفل مزامنة (sync.Mutex) لحماية الوصول للموارد المشتركة. يسمح للكوروتين بالدخول في حالة انتظار حتى يتحقق شرط معين، ثم يُوقظ عند تحققه.
مثال على الكود
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()، ليتابعوا التنفيذ.
البنية
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، قفل مزامنة، ليس القفل الذي مررناه، بل قفل داخلي فيruntimehead،tail، مؤشرات القائمة
له ثلاث طرق إجمالًا:
Wait، حجب وانتظارSignal، إيقاظ كوروتين منتظر واحدBroadcast، إيقاظ جميع الكوروتينات المنتظرة
معظم تنفيذه مخفي في مكتبة runtime، هذا التنفيذ موجود في ملف runtime/sema.go، لذلك الكود في المكتبة القياسية قصير جدًا، ومبدأه الأساسي هو طابور حجب مع قفل.
Wait
طريقة Wait تجعل الكوروتين يدخل في حالة حجب وانتظار حتى يُوقظ.
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، تحصل على فهرس آخر عنصر.
func notifyListAdd(l *notifyList) uint32 {
return l.wait.Add(1) - 1
}عملية الإضافة الحقيقية تتم في دالة notifyListWait.
func notifyListWait(l *notifyList, t uint32) {
...
}في هذه الدالة، أولًا تُقفل القائمة، ثم تتحقق بسرعة هل الكوروتين الحالي قد أُوقظ بالفعل، إذا أُوقظ تعود مباشرة بدون حاجة للحجب.
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.
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.
releaseSudog(s)Signal
Signal تُوقظ الكوروتين المحجوب حسب ترتيب الوصول أولًا يُخرج أولًا.
func (c *Cond) Signal() {
runtime_notifyListNotifyOne(&c.notify)
}عمليتها كالتالي:
بدون قفل تتحقق مباشرة، هل
l.waitيساويl.notify، إذا تساويا يعني أن جميع الكوروتينات أُوقظتgoif l.wait.Load() == atomic.Load(&l.notify) { return }بعد القفل، تتحقق مرة أخرى هل جميعها أُوقظت
golockWithRank(&l.lock, lockRankNotifyList) t := l.notify if t == l.wait.Load() { unlock(&l.lock) return }زيادة
l.notifyبواحدgoatomic.Store(&l.notify, t+1)تعبر القائمة دوريًا، تجد الكوروتين الذي يجب إيقاظه، وأخيرًا عبر
runtime.goreadyتُوقظ الكوروتين.gofor 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 تُوقظ جميع الكوروتينات المحجوبة.
func (c *Cond) Broadcast() {
runtime_notifyListNotifyAll(&c.notify)
}عمليتها متشابهة أساسًا:
فحص بدون قفل، هل جميعها أُوقظت
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 }قفل، إفراغ القائمة، ثم تحرير القفل، الكوروتينات الجديدة اللاحقة ستُضاف في رأس القائمة
golockWithRank(&l.lock, lockRankNotifyList) s := l.head l.head = nil l.tail = nil atomic.Store(&l.notify, l.wait.Load()) unlock(&l.lock)عبور القائمة، إيقاظ جميع الكوروتينات
gofor s != nil { next := s.next s.next = nil readyWithTime(s, 4) s = next }
ملخص
sync.Cond أكثر سيناريوهات استخدامًا شيوعًا هي الحاجة للمزامنة بين عدة كوروتينات حول شروط معينة، تُطبق عادةً في نموذج المنتج-المستهلك، وجدولة المهام، وغيرها. في هذه السيناريوهات، عدة كوروتينات تحتاج لانتظار تحقق شروط معينة لمتابعة التنفيذ، أو تحتاج للإشعار عند تغير الشروط. يوفر طريقة مرنة وفعالة لإدارة المزامنة بين الكوروتينات. بالاستخدام مع قفل المزامنة، sync.Cond يضمن أمان الوصول للموارد المشتركة، ويمكنه التحكم في ترتيب تنفيذ الكوروتينات عند تحقق شروط محددة. فهم مبادئ تنفيذه الداخلية يساعدنا على إتقان تقنيات البرمجة المتزامنة بشكل أفضل، خاصة عند التعامل مع مزامنة الشروط المعقدة.
