حماية البيانات المشتركة
يسمح sync.Mutex لـ goroutine واحدة في كل مرة بدخول الشيفرة بين Lock وUnlock. ضع الـ mutex بجوار البيانات التي يحرسها، عادة في البنية نفسها:
يطبع هذا دائمًا hits: 10000. دون القفل، ستُسقط 100 goroutine تكتب في الخريطة نفسها البرنامج عادة برسالة fatal error: concurrent map writes. هذا الخطأ يأتي من فحص في وقت التشغيل لا يضمن الاكتشاف دائمًا، ولا يمكن التعافي منه، وغياب الانهيار لا يثبت صحة الشيفرة.
تفاصيل مهمّة:
- القيمة الصفرية لـ
sync.Mutexغير مقفلة وجاهزة. لا حاجة لدالة بناء. - تستخدم التوابع مستقبِل مؤشر (
*Counter). مستقبِل القيمة سيقفل نسخة من الـ mutex، وهذا لا يحمي شيئًا. defer c.mu.Unlock()مباشرة بعدLockتعني أن كل مسار عودة، وكل panic، يحرّر القفل.- كل وصول يمرّ عبر القفل، بما في ذلك القراءات. القراءة دون قفل بينما تكتب goroutine أخرى ما زالت سباق بيانات.
أبقِ القسم الحرج صغيرًا
تفكّ defer القفل في نهاية الدالة. هذا صحيح للتوابع القصيرة مثل السابقة. أما في دالة أطول ففكّ القفل حالما تنتهي من لمس البيانات المشتركة، حتى لا تنتظر الـ goroutines الأخرى عملًا لا يحتاج القفل:
func (s *Store) Save(key string) error {
s.mu.Lock()
data := s.items[key] // copy what you need
s.mu.Unlock()
return writeToDisk(key, data) // slow I/O, outside the lock
}
الاحتفاظ بقفل عبر استدعاءات شبكية أو إدخال وإخراج على القرص أو إرسال على قناة هو أشيع سبب لبطء البرامج المتزامنة، والإرسال على قناة تحت قفل سبب شائع للجمود.
RWMutex للبيانات كثيرة القراءة
لـ sync.RWMutex وضعان. يأخذ RLock/RUnlock قفل قراءة مشتركًا يمكن لكثير من الـ goroutines حمله معًا. ويأخذ Lock/Unlock قفل الكتابة الحصري، الذي ينتظر حتى يغادر كل القرّاء.
يؤتي RWMutex ثماره عندما تهيمن القراءات وتؤدّي كل قراءة عملًا حقيقيًا تحت القفل. للأقسام الحرجة الصغيرة جدًا مثل بحث واحد في خريطة، يكون Mutex العادي غالبًا بالسرعة نفسها، لأن لقفل القراءة حساباته الخاصة. قِس الأداء قبل الاختيار.
لا يمكنك ترقية قفل قراءة إلى قفل كتابة. استدعاء Lock مع الاحتفاظ بـ RLock في الـ goroutine نفسها يسبّب جمودًا. حرّر قفل القراءة أولًا، ثم خذ قفل الكتابة وافحص الشرط مجددًا، لأن كاتبًا آخر قد يكون غيّر البيانات في الأثناء.
sync/atomic للقيم المفردة
لعدّاد أو علم واحد، sync/atomic أبسط وأرخص من mutex. الأغلفة ذات الأنواع (Go 1.19) هي ما يجب استخدامه:
تحمي العمليات الذرّية قيمة واحدة في كل مرة. حالما يجب أن تتغيّر قيمتان معًا (رصيد وعدد معاملات، أو خريطة وحجمها)، استخدم mutex. عمليتان ذرّيتان منفصلتان قد تتداخل بينهما goroutines أخرى.
sync.Once
تنفّذ sync.Once دالة مرة واحدة بالضبط، مهما كان عدد الـ goroutines التي تستدعيها في الوقت نفسه. كل من يستدعي Do ينتظر حتى ينتهي الاستدعاء الأول. وهي الطريقة المعتادة للتهيئة الكسولة:
كل سطر "runs once" يظهر مرة واحدة بالضبط. إذا سبّبت الدالة الممرّرة إلى Do حالة panic، تعدّها Once منتهية ولا تعيد المحاولة أبدًا. وتفعل sync.OnceValues الشيء نفسه للدوال التي تعيد قيمتين، عادة قيمة وخطأ.
Mutex أم قناة أم sync.Map
| الحالة | استخدم |
|---|---|
| بنية أو خريطة تحدّثها عدة goroutines في مكانها | sync.Mutex داخل البنية |
| قراءات في الغالب، وكتابات عرضية، والقراءات تؤدّي عملًا حقيقيًا | sync.RWMutex |
| عدّاد أو علم واحد | sync/atomic |
| تهيئة لمرة واحدة | sync.Once، sync.OnceValue |
| تسليم بيانات من goroutine إلى أخرى | قناة |
| ذاكرة مؤقّتة تُكتب مفاتيحها مرة وتُقرأ مرات كثيرة، أو goroutines تلمس مفاتيح منفصلة | sync.Map |
ليست sync.Map بديلًا عامًا لخريطة مقفلة. لا معاملات نوع لها، فتعود القيم كـ any، وهي أسرع فقط في الحالتين المذكورتين في الجدول. ابدأ بـ mutex وخريطة عادية.
أخطاء شائعة
- نسخ mutex. تمرير بنية تحتوي
sync.Mutexبالقيمة، أو استخدام مستقبِل قيمة، ينسخ القفل. يبلغgo vetعنpasses lock by valueأوcopies lock value. - القفل مرتين في goroutine واحدة. أقفال Go غير قابلة لإعادة الدخول. إذا استدعت
IncالدالةGetوأخذت كلتاهما القفل، تتوقّفIncإلى الأبد. اجعل التوابع العامة تقفل، والدوال المساعدة الخاصة تفترض أن القفل مأخوذ. - نسيان فكّ القفل عند عودة مبكرة. استخدم
deferما لم يكن لديك سبب لغير ذلك. - القفل بترتيبات مختلفة. إذا أخذت goroutine القفل A ثم B بينما أخذت أخرى B ثم A، قد تنتظر كل منهما الأخرى إلى الأبد. احصل دائمًا على الأقفال المتعدّدة بالترتيب نفسه.
- كشف البيانات المحروسة. إعادة الخريطة الداخلية من تابع تتيح للمستدعين قراءتها والكتابة فيها دون القفل. أعد نسخة (
maps.Clone، Go 1.21) أو قيمة واحدة. - حماية الكتابات وحدها. القراءات غير المقفلة المتزامنة مع كتابات مقفلة ما زالت سباقات. شغّل اختباراتك بـ
go test -race.
الأسئلة الشائعة
ما هو الـ mutex في Go؟
الـ sync.Mutex قفل يسمح لـ goroutine واحدة فقط في كل مرة بتنفيذ الشيفرة بين mu.Lock() وmu.Unlock(). تستخدمه لحماية بيانات تقرؤها وتكتبها عدة goroutines، مثل خريطة أو بنية. قيمته الصفرية mutex غير مقفل وجاهز للاستخدام.
متى أستخدم RWMutex بدل Mutex؟
عندما تفوق القراءات الكتابات بكثير وتحتفظ كل قراءة بالقفل مدة ذات معنى. يسمح RLock لأي عدد من القرّاء بالدخول معًا، بينما ينتظر Lock الوصول الحصري. للأقسام الحرجة القصيرة يكون Mutex العادي غالبًا بالسرعة نفسها أو أسرع، فقِس قبل التبديل.
هل الخريطة في Go آمنة للاستخدام المتزامن؟
لا. القراءات المتزامنة لا بأس بها، لكن الكتابة المتزامنة مع أي قراءة أو كتابة أخرى سباق بيانات، ويكتشفه وقت التشغيل عادة وينهار برسالة fatal error: concurrent map writes (أو concurrent map read and map write). احمِ الخريطة بـ sync.Mutex أو sync.RWMutex، أو استخدم sync.Map لحالات استخدامها المحدّدة.
هل sync.Mutex قابل لإعادة الدخول في Go؟
لا. إذا استدعت goroutine تملك القفل Lock مرة أخرى، تتوقّف إلى الأبد منتظرة نفسها. رتّب الشيفرة بحيث تأخذ التوابع المُصدَّرة القفل وتستدعي دوال مساعدة غير مُصدَّرة تفترض أنه مأخوذ مسبقًا.