واجه كل مبرمج يستخدم أكثر من مؤشر ترابط واحد في برنامجه بدايات التزامن. في سياق .NET ، هناك الكثير منها ، لن
أدرجها ، لقد قامت
MSDN بالفعل بهذا من أجلي.
اضطررت إلى استخدام العديد من هذه الأوليات ، وساعدت بشكل مثالي في التعامل مع المهام. لكن في هذه المقالة ، أود التحدث عن القفل المعتاد في تطبيق سطح المكتب وكيف ظهر البدائي الجديد (على الأقل بالنسبة لي) ، والذي يمكن تسميته PriorityLock.
المشكلة
عند تطوير تطبيق ذو مؤشرات ترابط متعددة التحميل ، يظهر مدير في مكان ما يعالج عدد لا يحصى من مؤشرات الترابط. لذلك كان معي. وعمل هذا المدير ، ومعالجة طن من الطلبات من عدة مئات من المواضيع. وكان كل شيء على ما يرام معه ، ولكن داخل القفل المعتاد عملت.
ثم في أحد الأيام ينقر مستخدم (على سبيل المثال ، I) على زر في واجهة التطبيق ، ينتقل الدفق إلى المدير (وليس دفق واجهة المستخدم بالطبع) ويتوقع أن يرى حفل استقبال ودود للغاية ، ولكن بدلاً من ذلك يقابله العمة كلافا من مكتب الاستقبال الأكثر كثافة في العيادة الأكثر كثافة بكلمات "لا أعطي لعنة من وجهك لدي 950 أكثر مثلك. اذهب واحصل عليها. لا يهمني كيف يمكنك معرفة ذلك. " هذه هي الطريقة التي يعمل القفل في .NET. ويبدو أن كل شيء على ما يرام ، سيتم تنفيذ كل شيء بشكل صحيح ، ولكن من الواضح أن المستخدم لم يخطط للانتظار لبضع ثوان للرد على تصرفه.
هذا هو المكان الذي تنتهي فيه القصة المحزنة ويبدأ حل المشكلة الفنية.
قرار
بعد أن درست البدائية القياسية ، لم أجد خيارًا مناسبًا. لذلك ، قررت كتابة القفل الخاص بي ، والذي سيكون له إدخال قياسي وعالي الأولوية. بالمناسبة ، بعد الكتابة ، درست nuget ، أيضًا ، لم أجد شيئًا من هذا القبيل ، على الرغم من أنني قد بحثت بشكل سيئ.
لكتابة مثل هذه البدائية (أو لم تعد بدائية) كنت بحاجة إلى عمليات SemaphoreSlim و SpinWait و Interlocked. في المفسد ، أشرت إلى الإصدار الأول من PriorityLock الخاص بي (الرمز المتزامن فقط ، لكنه الأهم) ، وشروحاته.
النص المخفيمن حيث التزامن ، لا توجد اكتشافات ، في حين أن شخص ما في القفل ، لا يمكن للآخرين الدخول. إذا جاءت أولوية عالية ، يتم دفعها إلى الأمام من قبل جميع أولئك الذين ينتظرون أولوية منخفضة.
فئة LockMgr ، يُقترح العمل بها في التعليمات البرمجية الخاصة بك. هو الذي هو موضوع التزامن ذاته. ينشئ كائنات Locker و HighLocker ، ويحتوي على أعمدة إشارة ، و SpinWait ، وعدادات ترغب في الدخول في القسم الحرج ، الخيط الحالي وعداد العودية.
public class LockMgr { internal int HighCount; internal int LowCount; internal Thread CurThread; internal int RecursionCount; internal readonly SemaphoreSlim Low = new SemaphoreSlim(1); internal readonly SemaphoreSlim High = new SemaphoreSlim(1); internal SpinWait LowSpin = new SpinWait(); internal SpinWait HighSpin = new SpinWait(); public Locker HighLock() { return new HighLocker(this); } public Locker Lock(bool high = false) { return new Locker(this, high); } }
تطبق الفئة Locker واجهة IDisposable. لتنفيذ العودية عند التقاط قفل ، نتذكر معرف الدفق ، ثم تحقق من ذلك. علاوة على ذلك ، وفقًا للأولوية ، في حالة الأولوية العالية ، نقول على الفور أننا وصلنا (زيادة عداد HighCount) ، والحصول على إشارة عالية ، والانتظار (إذا لزم الأمر) لتحرير القفل من الأولوية المنخفضة ، وبعد ذلك نحن مستعدون للحصول على القفل. في حالة أولوية منخفضة ، يتلقى إشارة منخفضة ، ثم ننتظر الانتهاء من جميع التدفقات ذات الأولوية العالية ، وأخذ إشارة عالية لفترة من الوقت ، زيادة LowCount.
تجدر الإشارة إلى أن معنى HighCount و LowCount مختلفان ، يعرض HighCount عدد مؤشرات الترابط ذات الأولوية التي تم قفلها ، عندما يعني LowCount فقط أن مؤشر الترابط (واحد فردي) ذو أولوية منخفضة قد دخل في القفل.
public class Locker : IDisposable { private readonly bool _isHigh; private LockMgr _mgr; public Locker(LockMgr mgr, bool isHigh = false) { _isHigh = isHigh; _mgr = mgr; if (mgr.CurThread == Thread.CurrentThread) { mgr.RecursionCount++; return; } if (_isHigh) { Interlocked.Increment(ref mgr.HighCount); mgr.High.Wait(); while (Interlocked.CompareExchange(ref mgr.LowCount, 0, 0) != 0) mgr.HighSpin.SpinOnce(); } else { mgr.Low.Wait(); while (Interlocked.CompareExchange(ref mgr.HighCount, 0, 0) != 0) mgr.LowSpin.SpinOnce(); try { mgr.High.Wait(); Interlocked.Increment(ref mgr.LowCount); } finally { mgr.High.Release(); } } mgr.CurThread = Thread.CurrentThread; } public void Dispose() { if (_mgr.RecursionCount > 0) { _mgr.RecursionCount--; _mgr = null; return; } _mgr.RecursionCount = 0; _mgr.CurThread = null; if (_isHigh) { _mgr.High.Release(); Interlocked.Decrement(ref _mgr.HighCount); } else { _mgr.Low.Release(); Interlocked.Decrement(ref _mgr.LowCount); } _mgr = null; } } public class HighLocker : Locker { public HighLocker(LockMgr mgr) : base(mgr, true) { } }
باستخدام كائن فئة LockMgr كان موجزا جدا. يوضح المثال بوضوح إمكانية إعادة استخدام _lockMgr داخل القسم الحرج ، في حين أن الأولوية لم تعد مهمة.
private PriorityLock.LockMgr _lockMgr = new PriorityLock.LockMgr(); public void LowPriority() { using (_lockMgr.Lock()) { using (_lockMgr.HighLock()) {
لذلك أنا حل مشكلتي. بدأ معالجة إجراءات المستخدم التي يتعين القيام بها مع أولوية عالية ، لم يصب أحد ، وفاز الجميع.
اتواقت
نظرًا لأن كائنات فئة SemaphoreSlim تدعم الانتظار غير المتزامن ، فقد أضفت هذه الفرصة أيضًا إلى نفسي. يختلف الرمز إلى الحد الأدنى ، وفي نهاية المقال ، سأقدم رابطًا إلى الكود المصدري.
من المهم أن نلاحظ هنا أن المهمة ليست مرتبطة بالموضوع بأي شكل من الأشكال ، لذلك لا يمكن تنفيذ إعادة الاستخدام غير المتزامن للقفل بطريقة مماثلة. علاوة على ذلك ، لا
تضمن خاصية
Task.CurrentId كما هو موضح بواسطة MSDN أي شيء. هذا هو المكان الذي انتهت فيه خياراتي.
بحثًا عن حل ، صادفت مشروع
NeoSmart.AsyncLock ، حيث تم الإشارة إلى دعم إعادة استخدام القفل غير المتزامن. من الناحية الفنية ، إعادة استخدام الأعمال. لكن لسوء الحظ ، فإن القفل نفسه ليس قفلًا. كن حذرا إذا كنت تستخدم هذه الحزمة ، كن على علم أنه لا يعمل بشكل صحيح!
استنتاج
والنتيجة هي فئة تدعم العمليات المتزامنة مع إعادة الاستخدام ، والعمليات غير المتزامنة دون إعادة استخدامها. يمكن استخدام العمليات غير المتزامنة والمتزامنة جنبًا إلى جنب ، ولكن لا يمكن استخدامها معًا! كل ذلك بسبب عدم وجود دعم لإعادة استخدام الخيار غير المتزامن.
آمل ألا أكون وحدي في مثل هذه المشكلات وأن يكون حلي مفيدًا لشخص ما. لقد نشرت المكتبة على جيثب و nuget.
توجد اختبارات في المستودع تظهر صحة PriorityLock. في الجزء غير المتزامن من هذا الاختبار ، تم اختبار NeoSmart.AsyncLock وفشل الاختبار.
رابط إلى nugetرابط جيثب