Go / المتقدم: السلامة والأداء وتصميم الخدمات / الدرس 13
سلامة السباقات والمزامنة
امنح الحالة المشتركة سياسة وصول متماسكة وبرر الملاحظات بالمزامنة لا بتخمين الجدولة.
في هذه الصفحة
أهداف الدرس
- تحديد الوصولات المتعارضة دون مزامنة.
- حماية الثوابت المركبة بقفل.
- استخدام الذريات دون افتراض ذرية المعاملات المركبة.
الملاحظة الآمنة تحتاج حجة ترتيب
يحدث سباق عندما تصل روتينات للذاكرة نفسها بالتزامن وأحد الوصولات كتابة دون ترتيب بالمزامنة. يمنح نموذج Go اتساقا تسلسليا للبرامج الخالية من سباق البيانات. لا تنسب نتيجة حتمية مريحة لبرنامج متسابق ولا تعد جدولة ناجحة برهان سلامة.
تنشئ القنوات والأقفال والذريات علاقات مزامنة موثقة؛ ولا يفعل الانتظار قليلا ذلك. قد يجعل انتظار النهاية قراءة لاحقة آمنة لكنه لا يصلح كتابات تعارضت أثناء نشاط العاملين. راجع كل كائن مشترك وكل وصول إليه.
حماية كل أجزاء عملية الخريطة المشتركة
يحرس المثال الأول كل زيادة خريطة بقفل واحد وينتظر العاملين قبل التقرير. يغطي القفل القراءة والتعديل والكتابة، لا الإسناد وحده. ليست الخريطة العادية آمنة لكتابات متداخلة أو كتابة مع قراءة. تصح القراءات فقط بعد اكتمال الإنشاء والنشر الآمن.
يحمي القفل ثابتا بالاتفاق: يجب التزام كل شيفرة تصل للحالة بالقاعدة. قد يتيح إرجاع خريطة أو شريحة خام تجاوزها بعد فتح القفل. فضل لقطة منسوخة أو عمليات تبقي الوصول داخل المكون.
package main
import (
"fmt"
"sync"
)
func main() {
counts := map[string]int{"jobs": 0}
var mu sync.Mutex
var workers sync.WaitGroup
workers.Add(2)
for worker := 0; worker < 2; worker++ {
go func() {
defer workers.Done()
for i := 0; i < 1000; i++ {
mu.Lock()
counts["jobs"]++
mu.Unlock()
}
}()
}
workers.Wait()
mu.Lock()
result := counts["jobs"]
mu.Unlock()
fmt.Println("jobs", result)
}
jobs 2000
تُحمى كل زيادة كاملة ويثبت Wait اكتمال التحديثات الألفين.
استخدام تحديث ذري للعداد
توفر sync/atomic عمليات ذرية بترتيب متسق تسلسليا حسب الوثائق. قد تضيع زيادة مع Load ثم Store للقيمة القديمة زائد واحد رغم ذرية كل منهما. تنفذ Add تحديث قراءة وتعديل وكتابة واحدا. اختر قفلا لعدة حقول ذات ثابت واحد بدلا من جمع ذريات دون برهان.
يستخدم المثال الثاني atomic.Int64 المتاح قبل الأساس وينتظر قبل Load النهائية. لا تنسخ القيم الذرية ذات الأنواع بعد أول استخدام. ولا يجعل الوصول الذري المجموعات المشار إليها ثابتة أو يحرر موارد أو يمنح معاملة الفحص ثم الفعل دلالتها اللازمة تلقائيا.
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var count atomic.Int64
var workers sync.WaitGroup
workers.Add(2)
for worker := 0; worker < 2; worker++ {
go func() {
defer workers.Done()
for i := 0; i < 1000; i++ {
count.Add(1)
}
}()
}
workers.Wait()
fmt.Println("count", count.Load())
}
count 2000
كل Add تحديث ذري واحد فتحتوي النتيجة بعد الانتظار ألف زيادة من كل حلقة.
تشمل سياسة الأقفال النسخ والترتيب
لا تنسخ sync.Mutex أو RWMutex بعد أول استخدام. استخدم مستقبل مؤشر لنوع يحمل أقفالا ولا تعده قيمة بعد الاستخدام. أضف عمل WaitGroup قبل الإطلاق لا من داخل روتين قد يتسابق مع Wait التي ترى صفرا. تستخدم الدورة Add/Done الأساسية لا مساعدات أحدث.
إذا لزمت أقفال متعددة فحدد ترتيب اقتناء ثابت لمنع الجمود. اجعل القسم الحرج قصيرا لكنه كاملا؛ قد يكسر إخراج قراءة لازمة الصحة. ليست RWMutex أسرع تلقائيا، ولـsync.Map حالات متخصصة لا بديل عام لكل خريطة وقفل.
كاشف السباق شاهد للمسارات المنفذة
شغل go test -race أو build -race على منصة مدعومة بالأدوات اللازمة. تحدد التقارير الوصولات المتعارضة ومكدسات الإنشاء فتساعد الملكية. يلاحظ الكاشف المسارات والجداول المنفذة فقط، فالصمت دليل لا برهان لكل المسارات.
أبق المقاطع المتسابقة عمدا غير قابلة للتشغيل تعليميا. اختبر الثابت المصحح بتحديثات متزامنة ونتيجة مستقرة ثم راجع تغطية القفل والإنهاء. فشل الخريطة المتزامن عرض خطير لكن غيابه لا يثبت السلامة واسترداد هلع ليس مزامنة.
الأخطاء الشائعة وإصلاحها
يُعد Wait الأخير إصلاحا لزيادات متسابقة.
- لماذا يحدث؟
- اختلط ترتيب الاكتمال بالحماية بين العاملين.
- كيف تصلحه؟
- زامن كل تحديث متعارض أثناء العمل.
- كيف تتجنب تكراره؟
- ارسم الترتيب بين الوصولات المتعارضة فعلا.
لقفل كائن منسوخ أثر غير صحيح.
- لماذا يحدث؟
- نُسخ القفل بعد الاستخدام أو عبر مستقبل قيمة.
- كيف تصلحه؟
- استخدم كائنا ثابتا ومستقبلات مؤشرات.
- كيف تتجنب تكراره؟
- شغل vet وراجع نسخ الأنواع الحاملة للمزامنة.
تضيع زيادة رغم الذريات.
- لماذا يحدث؟
- عوملت Load وStore المنفصلتان كمعاملة واحدة.
- كيف تصلحه؟
- استخدم Add أو احم العملية المركبة.
- كيف تتجنب تكراره؟
- راجع الثابت كله لا التعليمات منفردة.
اختبر فهمك
1. تحمي ذاكرة مخبأة الكتابات وتقرأ بلا قفل لسرعتها. اشرح العيب وتصميمين صحيحين.
اعرض الحل المشروح
تبقى القراءة المتداخلة مع كتابة متعارضة مهما قصرت. احم الاثنين بسياسة قفل واحدة وربما RWMutex مبررة، أو انشر لقطات كاملة ثابتة بمزامنة صحيحة ولا تعدل المنشور. عالج أيضا إرجاع الأسماء البديلة. لا تثبت السرعة ترتيبا.
2. لحساب رصيد ذري وعدد معاملات ذري. لماذا قد لا تعطي قراءتهما لقطة معاملة متماسكة؟
اعرض الحل المشروح
كل عملية ذرية لكن قد يحدث تحديث بين القراءتين فيمثل الزوج مرحلتين مختلفتين. إذا لزم الاتساق فاحم الحقلين وتحديثهما بقفل واحد أو بروتوكول لقطة متحقق منه. لا تجعل ذريتان ثابت الحقول معاملة تلقائيا.
أهم ما تعلمته
- زامن كل وصول متعارض بسياسة مفهومة واحدة.
- يحلان الاكتمال والاستبعاد المتبادل مشكلتين مختلفتين.
- لا تعني ذرية الحقول معاملة مركبة.