C# / المتقدم: الأعمار والتزامن وتصميم التنفيذ / الدرس 15
التزامن والمزامنة
احم ثوابت الحالة المشتركة كاملة وميز العملية الآمنة من معاملة التطبيق.
في هذه الصفحة
أهداف الدرس
- تحديد الوصولات المشتركة المتعارضة.
- استخدام قفل ثابت أوذرية بنطاقها.
- تحديد الوصول غير المتزامن بالملكية والاكتمال.
قفل التحديث المركب كاملا
يستخدم المثال الأول بوابة خاصة واحدة لكل زيادة. يبدأ العاملان وينتظرهما المالك، ثم يقرأ النتيجة تحت القاعدة نفسها. إذا قرئت القيمة القديمة خارج القفل وقُفل الإسناد فقط فقد تضيع تحديثات. يلائم الحمل الثابت int وينتهي دون الاعتماد على توقيت الجدولة.
استخدم كائن قفل مستقرا ومخصصا، لا this أو كائنا عاما أو نصا داخليا قد تقفله شيفرة أخرى. توفر البيئة الحديثة System.Threading.Lock أيضا، وتظل صيغة القفل على object مفيدة ومفهومة. لا يجوز await داخل تعليمة lock؛ اختر تصميما ملائما للانتظار غير المتزامن عند الحاجة.
using System;
using System.Threading.Tasks;
object gate = new object();
int count = 0;
Task first = Task.Run(Increment);
Task second = Task.Run(Increment);
await Task.WhenAll(first, second);
lock (gate) Console.WriteLine($"count={count}");
void Increment()
{
for (int i = 0; i < 1000; i++)
{
lock (gate) count++;
}
}
count=2000
تحمل كل زيادة البوابة نفسها فتحفظ التحديثات الألفين.
اختيار تحديث ذري واحد
تنفذ Interlocked.Increment زيادة ذرية على المتغير المحدد. ليست قراءة القيمة ثم إسناد القديمة زائد واحد العملية نفسها. تناسب الذريات انتقالات حالة صغيرة محددة، ولا تجعل عدة حقول ذرية لقطة متماسكة أو معاملة تلقائيا. استخدم قفلا عندما يوضح الثابت المطلوب.
يستخدم المثال الثاني Interlocked للتحديث وVolatile.Read للقراءة الأخيرة، وتظل await مسؤولة عن ملاحظة الاكتمال. لا تمثل volatile وحدها زيادة ذرية ولا تجعل مجموعة آمنة للخيوط. تجنب حيل ترتيب الذاكرة دون حجة كاملة تشمل كل الوصولات ونشر الكائنات.
using System;
using System.Threading;
using System.Threading.Tasks;
int count = 0;
Task first = Task.Run(Increment);
Task second = Task.Run(Increment);
await Task.WhenAll(first, second);
Console.WriteLine($"count={Volatile.Read(ref count)}");
void Increment()
{
for (int i = 0; i < 1000; i++) Interlocked.Increment(ref count);
}
count=2000
تساهم كل عملية مرة وينتهي العاملان قبل التقرير.
لا تجعل الطرق الآمنة سير العمل ذرياً
توفر ConcurrentDictionary عمليات فردية آمنة وفق قواعدها، لكن ContainsKey متبوعة بتحديث الفهرس ليست معاملة واحدة تلقائيا. قد تنفذ مفوضات GetOrAdd وAddOrUpdate أكثر من مرة وخارج الأقفال الداخلية. لذلك لا تجعل مصانع القيم تنفذ آثارا غير قابلة للتكرار بافتراض استدعاء واحد.
تحد SemaphoreSlim.WaitAsync العمل غير المتزامن النشط. حرر الإذن بعد نجاح الحصول عليه فقط، وعادة داخل finally. يحد السمافور النشاط لا صفا منتظرا غير محدود. أبقه حيا حتى ينتهي المستعيرون، وحدد الإلغاء عند القبول وسياسة الحمل الزائد بدلا من إخفاء انتظار بلا حد.
مراجعة الترتيب والعمر والتقدم معا
تحتاج الأقفال المتعددة إلى ترتيب اقتناء متسق. لا تحمل قفلا وأنت تستدعي سلوكا مجهولا أو تحجب على عمل غير متزامن إلا بحجة مدروسة تمنع الجمود. قلل التعديل المشترك برسائل ثابتة أو ملكية حصرية عندما يبسط ذلك التصميم. وثق سلامة الخيوط لكل عملية ولا تستنتجها من اسم مساحة أسماء.
نسق بداية العاملين واكتمالهم بإشارات صريحة في الاختبارات، واستخدم حملا اصطناعيا محدودا وافحص الثابت بعد انتهاء الجميع. تكرار النجاح دليل لا برهان شامل. استخدم المكدسات وأدوات التنفيذ لتشخيص التعليق، واحفظ اختبار تراجع لسياسة الوصول التي انكسرت أولا لا للعرض الظاهر وحده.
الأخطاء الشائعة وإصلاحها
يضيع العداد رغم WhenAll.
- لماذا يحدث؟
- تسابق العاملون قبل الانتظار.
- كيف تصلحه؟
- زامن التحديث كاملا بقفل أوInterlocked.
- كيف تتجنب تكراره؟
- راجع الوصول أثناء العمل.
ينفذ المصنع أثرا مرتين.
- لماذا يحدث؟
- افتُرض تنفيذ واحد لمفوض قد يتكرر.
- كيف تصلحه؟
- اجعله آمنا أوافصل الأثر ببروتوكول.
- كيف تتجنب تكراره؟
- اختبر التنافس ووثق الدلالة.
يفرط السمافور بالتحرير بعد الإلغاء.
- لماذا يحدث؟
- يحرر رغمعدم اقتناء WaitAsync.
- كيف تصلحه؟
- حرر بعدنجاح فقط.
- كيف تتجنب تكراره؟
- اختبر الإلغاء المسبق وأثناء الانتظار.
اختبر فهمك
1. يحدث الرصيد وعدد المعاملات بعمليتين ذريتين منفصلتين. لماذا قد يرى القارئ زوجا لا يمثل معاملة مكتملة؟
اعرض الحل المشروح
يمكن أن يقع تحديث آخر بين قراءتي الحقلين أو كتابتيهما. لا تجعل ذرية كل حقل الزوج معاملة واحدة. إذا طلبت الواجهة زوجا متماسكا فاحم الحقلين وتحديثهما بقفل واحد، أو استخدم بروتوكول لقطات ثابتة مثبت الصحة. وثق حد اللقطة واختبر القراء والكتاب معا.
2. ماذا يفعل نمط SemaphoreSlim آمن للإلغاء إذا وقع الإلغاء قبل اقتناء الإذن؟
اعرض الحل المشروح
لا يحرر إذنا لم يحصل عليه. انتظر WaitAsync أولا، ثم ادخل try/finally لا تنفذ التحرير إلا بعد نجاح الاقتناء. افصل تصنيف الإلغاء عن فشل العملية، وانتظر كل المستخدمين قبل التخلص من السمافور. وحد الطلب المنتظر مستقلا عن عدد الأذونات.
أهم ما تعلمته
- احم الثوابت كاملة لاالإسناد منفردا.
- تحل أدوات الاكتمال والذرية والقفل والقبول مشكلات مختلفة.
- تحتاج المصانع والاستدعاءات والأقفال والتخلص أعمارا متوافقة.