.NET: أدوات للعمل مع multithreading و التزامن. الجزء 2

أنشر المقال الأصلي عن هبر ، والذي يتم نشر ترجمته على مدونة Codingsight .

أواصل إنشاء نسخة نصية من حديثي في ​​الاجتماع متعدد مؤشرات الترابط. يمكن العثور على الجزء الأول هنا أو هنا ، وكان هناك المزيد حول المجموعة الأساسية من الأدوات لبدء سلسلة أو مهمة ، وطرق لعرض حالتها وبعض الأشياء الصغيرة الحلوة مثل PLinq. في هذه المقالة ، أود التركيز بشكل أكبر على المشكلات التي قد تنشأ في بيئة متعددة الخيوط وبعض الطرق لحلها.

محتوى





حول الموارد المشتركة


من المستحيل كتابة برنامج يعمل في عدة مؤشرات ترابط ، ولكن في نفس الوقت لن يكون له مورد مشترك واحد: حتى إذا نجحت في مستوى تجريدك ، فإن النزول إلى مستوى واحد أو أكثر أدناه يوضح أنه لا يزال هناك مورد مشترك. سأقدم بعض الأمثلة:

مثال رقم 1:

خوفًا من حدوث مشكلات محتملة ، قمت بعمل سلاسل الرسائل مع ملفات مختلفة. حسب الملف لتيار. يبدو لك أن البرنامج لا يحتوي على مورد مشترك واحد.

بعد انخفاض مستويات عدة أدناه ، نفهم أن هناك محرك أقراص ثابت واحد فقط ، وسيتعين على برنامج التشغيل أو نظام التشغيل الخاص به حل مشاكل ضمان الوصول إليه.

مثال رقم 2:

بعد قراءة المثال رقم 1 ، قررت وضع الملفات على جهازين بعيدين مختلفين مع قطعتين مختلفتين فعليًا من الحديد وأنظمة التشغيل. نبقي 2 اتصالات مختلفة عبر بروتوكول نقل الملفات أو NFS.

عند النزول إلى عدة مستويات أدناه ، ندرك أنه لم يتغير شيء ، وسيتعين على برنامج تشغيل بطاقة الشبكة أو نظام تشغيل الجهاز الذي يعمل عليه البرنامج حل مشكلة الوصول التنافسي.

مثال رقم 3:

بعد أن فقدت جزءًا كبيرًا من شعرك في محاولات لإثبات إمكانية كتابة برنامج متعدد الخيوط ، فإنك ترفض الملفات تمامًا وتتحلل من العمليات الحسابية إلى كائنين مختلفين ، تتوفر ارتباطات لكل منها فقط لدفق واحد.

أظن أن المسامير النهائية العشر في نعش هذه الفكرة هي: وقت تشغيل واحد وجمع القمامة ، وجدولة مؤشر الترابط ، ذاكرة الوصول العشوائي RAM واحدة والذاكرة ، ومعالج واحد لا تزال موارد مشتركة.

لذلك ، اكتشفنا أنه من المستحيل كتابة برنامج متعدد الخيوط بدون مورد مشترك واحد على جميع مستويات التجريد عبر عرض مكدس التقنية بالكامل. لحسن الحظ ، كل مستوى من مستويات التجريد ، كقاعدة عامة ، يحل جزئياً أو كلياً مشاكل الوصول التنافسي أو يحظره ببساطة (على سبيل المثال: يمنع إطار عمل واجهة المستخدم العمل مع عناصر من مؤشرات ترابط مختلفة) ، وبالتالي تنشأ المشاكل في الغالب مع الموارد المشتركة على مستواك في التجريد. لحلها أعرض مفهوم التزامن.

المشاكل المحتملة عند العمل في بيئة متعددة الخيوط


يمكن تقسيم الأخطاء في البرنامج إلى عدة مجموعات:

  1. البرنامج لا ينتج نتيجة. تعطل أو يتجمد.
  2. البرنامج بإرجاع نتيجة غير صحيحة.
  3. ينتج البرنامج النتيجة الصحيحة ، لكنه لا يفي بمتطلبات أخرى غير وظيفية. يعمل لفترة طويلة أو يستهلك الكثير من الموارد.

في بيئة متعددة مؤشرات الترابط ، جهازي المشاكل الرئيسية تسبب الأخطاء 1 و 2 حالة توقف تام وشرط السباق .

مأزق


حالة توقف تام - حالة توقف تام. هناك العديد من الاختلافات المختلفة. الاكثر شيوعا هي ما يلي:



أثناء قيام مؤشر الترابط رقم 1 بعمل شيء ما ، تم حظر المورد مؤشر ترابط رقم 2 ، ثم تم حظر مورد مؤشر الترابط رقم 1 لاحقًا ومحاولة تأمين المورد B ، للأسف لن يحدث هذا أبدًا ، لأن مؤشر ترابط # 2 سيصدر المورد B فقط بعد تأمين المورد A.

حالة السباق


حالة سباق - حالة سباق. الموقف الذي فيه سلوك ونتائج العمليات الحسابية التي أجراها البرنامج يعتمد على عمل جدولة مؤشر ترابط وقت التشغيل.
يكمن عدم الارتياح لهذا الموقف على وجه التحديد في حقيقة أن البرنامج الخاص بك قد لا يعمل مرة واحدة فقط من بين مئات أو حتى من مليون شخص.

يتفاقم الموقف من خلال حقيقة أن المشاكل يمكن أن تسير معًا ، على سبيل المثال: مع وجود سلوك معين لجدولة مؤشر الترابط ، تحدث حالة توقف تام.

بالإضافة إلى هاتين المشكلتين المؤديتين إلى حدوث أخطاء واضحة في البرنامج ، هناك أيضًا مشكلات قد لا تؤدي إلى نتيجة حسابية غير صحيحة ، ولكن سيتم إنفاق المزيد من الوقت أو قوة المعالجة للحصول عليها. اثنين من هذه المشاكل هي: مشغول الانتظار و Starveation مؤشر الترابط .

مشغول-الانتظار


مشغول-الانتظار هي مشكلة يستهلك فيها البرنامج موارد المعالج ليس للحسابات ولكن للانتظار.

غالبًا ما تبدو هذه المشكلة في التعليمات البرمجية كالتالي:

while(!hasSomethingHappened) ; 

هذا مثال على الكود السيئ للغاية منذ ذلك الحين تشغل هذه الشفرة تمامًا أحد النواة في المعالج بينما لا تفعل شيئًا مفيدًا على الإطلاق. يمكن تبريره إذا كان من المهم للغاية معالجة تغيير في قيمة ما في سلسلة رسائل أخرى ، وفقط إذا كان من المهم للغاية. وأتحدث بسرعة ، أنا أتحدث عن الحالة عندما لا يمكنك الانتظار حتى بضع ثوانٍ من النانو. في حالات أخرى ، أي في كل شيء يمكن أن ينتج دماغًا صحيًا ، يكون من المعقول استخدام أنواع ResetEvent وإصداراتها النحيفة. عنهم أدناه.

ربما سيقترح أحد القراء حل مشكلة تحميل نواة واحدة بالكامل مع انتظار عديم الفائدة عن طريق إضافة تصميمات مثل Thread.Sleep (1) إلى الحلقة. سيؤدي هذا بالفعل إلى حل المشكلة ، ولكنه سيخلق مشكلة أخرى: سيكون متوسط ​​الاستجابة للتغير نصف مللي ثانية ، وهو ما قد لا يكون كبيرًا ، ولكن قد يكون كارثيًا أكثر مما يمكنك استخدام بدائل التزامن لعائلة ResetEvent.

تجويع الموضوع


مؤشر ترابط Starvation يمثل مشكلة حيث يوجد في البرنامج العديد من مؤشرات الترابط التي تعمل في وقت واحد. ماذا يعني بالضبط تلك التدفقات التي تكون مشغولة بالحسابات ، وليس فقط انتظار استجابة من أي IO. مع هذه المشكلة ، يتم فقد كل مكاسب الأداء المحتملة من استخدام مؤشرات الترابط ، لأن يقضي المعالج الكثير من الوقت في تبديل السياقات.
من المريح البحث عن مثل هذه المشكلات باستخدام ملفات التعريف المختلفة ، فيما يلي مثال على لقطة شاشة من ملف التعريف dotTrace الذي تم إطلاقه في وضع الجدول الزمني.


(الصورة قابلة للنقر)

في البرنامج الذي لا يعاني من تدفق الجوع ، لن يكون هناك لون وردي على الرسوم البيانية التي تعكس التدفقات. بالإضافة إلى ذلك ، في فئة الأنظمة الفرعية ، من الواضح أن 30.6٪ من البرنامج كان ينتظر وحدة المعالجة المركزية.

عند تشخيص مثل هذه المشكلة ، يتم حلها ببساطة شديدة: لقد بدأت عددًا كبيرًا من مؤشرات الترابط في وقت واحد ، وابدأ أقل أو لم يكن جميعها دفعة واحدة.

أدوات المزامنة



مشابك


ربما هذه هي الطريقة الأكثر خفيفة الوزن للمزامنة. المتشابكة هي عبارة عن مجموعة من العمليات الذرية البسيطة. العملية الذرية تسمى العملية التي لا يمكن أن يحدث فيها شيء. في .NET ، يتم تمثيل Interlocked بواسطة فئة ثابتة بنفس الاسم مع عدد من الأساليب ، كل منها ينفذ عملية ذرية واحدة.

لإدراك رعب العمليات غير الذرية ، حاول كتابة برنامج يبدأ بـ 10 سلاسل ، كل منها ينتج مليون زيادة من نفس المتغير ، وفي نهاية عملهم ، قم بطباعة قيمة هذا المتغير - لسوء الحظ ، سيكون مختلفًا تمامًا عن 10 ملايين ، علاوة على ذلك في كل مرة يبدأ البرنامج ، سيكون مختلفًا. يحدث هذا لأنه حتى مثل هذه العملية البسيطة مثل الزيادة ليست ذرية ، ولكنها تتضمن استخراج قيمة من الذاكرة وحساب واحدة جديدة والكتابة مرة أخرى. وبالتالي ، يمكن أن اثنين من مؤشرات الترابط في وقت واحد القيام بكل من هذه العمليات ، وفي هذه الحالة سيتم فقدان الزيادة.

توفر الفئة Interlocked أساليب زيادة / إنقاص ؛ من السهل تخمين ما تفعله. أنها مريحة للاستخدام إذا كنت تقوم بمعالجة البيانات في مؤشرات ترابط متعددة وتفكر في شيء ما. مثل هذا الرمز سوف يعمل بشكل أسرع بكثير من القفل الكلاسيكي. إذا تم استخدام Interlocked للحالة الموضحة في الفقرة الأخيرة ، فسيقوم البرنامج بإعطاء 10 ملايين شخص بشكل ثابت في أي موقف.

تؤدي طريقة CompareExchange ، للوهلة الأولى ، وظيفة غير واضحة إلى حد ما ، ولكن كل وجودها يسمح لك بتنفيذ العديد من الخوارزميات المثيرة للاهتمام ، وخاصة الأسرة الخالية من القفل.

 public static int CompareExchange (ref int location1, int value, int comparand); 

تأخذ الطريقة ثلاث قيم: الأولى يتم تمريرها بالرجوع وهذه هي القيمة التي سيتم تغييرها إلى الثانية ، إذا تطابق وقت المقارنة 1 مع المقارنة ، فسيتم إرجاع القيمة الأصلية للموقع 1. يبدو الأمر مربكًا إلى حد ما ، لأنه من الأسهل كتابة التعليمات البرمجية التي تنفذ نفس العمليات مثل CompareExchange:

 var original = location1; if (location1 == comparand) location1 = value; return original; 

سيكون تنفيذ في الفئة Interlocked فقط الذرية. وهذا هو ، إذا كتبنا هذه الشفرة بأنفسنا ، فقد يحدث موقف عندما تم بالفعل استيفاء الشرط location1 == المقارنة ، ولكن بحلول الوقت الذي تم فيه تنفيذ location1 = تعبير القيمة ، غيّر مؤشر ترابط آخر قيمة location1 وسيُفقد.

يمكننا العثور على مثال جيد لاستخدام هذه الطريقة في التعليمات البرمجية التي ينشئها المترجم لأي حدث C #.

دعنا نكتب فئة بسيطة مع حدث MyEvent واحد:

 class MyClass { public event EventHandler MyEvent; } 

دعنا نبني المشروع في تكوين Release ونفتح التجميع باستخدام dotPeek مع تشغيل خيار Show Compiler Generated Code:

 [CompilerGenerated] private EventHandler MyEvent; public event EventHandler MyEvent { [CompilerGenerated] add { EventHandler eventHandler = this.MyEvent; EventHandler comparand; do { comparand = eventHandler; eventHandler = Interlocked.CompareExchange<EventHandler>(ref this.MyEvent, (EventHandler) Delegate.Combine((Delegate) comparand, (Delegate) value), comparand); } while (eventHandler != comparand); } [CompilerGenerated] remove { // The same algorithm but with Delegate.Remove } } 

هنا يمكنك أن ترى أنه خلف الكواليس ، قام المترجم بإنشاء خوارزمية معقدة إلى حد ما. تحمي هذه الخوارزمية من حالة فقدان اشتراك حدث عندما تشترك عدة مؤشرات ترابط في هذا الحدث في وقت واحد. دعنا نكتب طريقة الإضافة بمزيد من التفصيل ، مع تذكر ما تقوم به طريقة CompareExchange وراء الكواليس

 EventHandler eventHandler = this.MyEvent; EventHandler comparand; do { comparand = eventHandler; // Begin Atomic Operation if (MyEvent == comparand) { eventHandler = MyEvent; MyEvent = Delegate.Combine(MyEvent, value); } // End Atomic Operation } while (eventHandler != comparand); 

هذا بالفعل أكثر وضوحًا قليلاً ، رغم أنه ربما لا يزال بحاجة إلى شرح. في الكلمات ، أود أن أصف هذه الخوارزمية على النحو التالي:

إذا كان MyEvent لا يزال كما كان في الوقت الذي بدأنا فيه تشغيل المفوض. كومبين ، فقم بتدوين ما يعود إليه المندوب. ويعود كومبين ، وإذا لم يكن الأمر كذلك ، فلا يهم ، دعنا نحاول مرة أخرى ونكرر ذلك حتى يخرج.


لذلك لن يتم فقدان اشتراك الحدث. سيكون عليك حل مشكلة مماثلة إذا كنت تريد فجأة تطبيق صفيف خالٍ من قفل مؤشر ترابط آمن. إذا اندفعت عدة تدفقات لإضافة عناصر إليها ، فمن المهم أن تضاف جميعها في النهاية.

Monitor.Enter ، Monitor.Exit ، قفل


هذه هي البنى الأكثر استخدامًا لمزامنة مؤشر الترابط. إنهم يقومون بتطبيق فكرة قسم مهم: بمعنى أنه يمكن تنفيذ التعليمات البرمجية المكتوبة بين المكالمات إلى Monitor.Enter و Monitor.Exit على مورد واحد في وقت واحد في مؤشر ترابط واحد فقط. بيان القفل عبارة عن سكر نحوي حول مكالمات Enter / Exit ملفوفة في محاولة أخيرة. تتمثل إحدى الميزات الجيدة لتطبيق قسم هام في .NET في القدرة على إعادة إدخاله لنفس الدفق. هذا يعني أن مثل هذا الكود سوف ينفذ دون مشاكل:

 lock(a) { lock (a) { ... } } 

من غير المحتمل ، بطبيعة الحال ، أن يكتب شخص ما بهذه الطريقة ، لكن إذا قمت بتشفير هذه الشفرة في عدة طرق بعمق مكدس استدعاء هذه الميزة يمكن أن توفر لك بعض ifs. لجعل هذه الخدعة ممكنة ، كان على مطوري .NET إضافة قيود - يمكن فقط استخدام مثيل لنوع المرجع ككائن التزامن ، وتتم إضافة عدة بايت ضمنيًا إلى كل كائن حيث سيتم كتابة معرف الدفق.

تفرض ميزة القسم المهم في c # قيدًا مثيرًا للاهتمام واحد على تشغيل بيان القفل: لا يمكنك استخدام بيان الانتظار داخل بيان القفل. في البداية ، فاجأني ذلك ، لأن هناك مجموعة متشابهة من إنشاءات Monitor.Enter / Exit مماثلة. ما هو الموضوع؟ من الضروري هنا إعادة قراءة الفقرة الأخيرة بعناية مرة أخرى ، ثم إضافة بعض المعرفة حول مبدأ عدم التزامن / الانتظار: لن يتم تنفيذ التعليمات البرمجية بعد الانتظار بالضرورة على نفس مؤشر الترابط مثل الرمز قبل الانتظار ، بل يعتمد على سياق التزامن والحضور أو لا توجد دعوة إلى ConfigureAwait. يترتب على ذلك أنه يمكن تنفيذ Monitor.Exit على مؤشر ترابط غير Monitor.Enter ، والذي سيلقي SynchronizationLockException . إذا كنت لا تصدق ذلك ، فيمكنك تنفيذ التعليمات البرمجية التالية في تطبيق وحدة التحكم: سيؤدي ذلك إلى طرح SynchronizationLockException.

 var syncObject = new Object(); Monitor.Enter(syncObject); Console.WriteLine(Thread.CurrentThread.ManagedThreadId); await Task.Delay(1000); Monitor.Exit(syncObject); Console.WriteLine(Thread.CurrentThread.ManagedThreadId); 

من الجدير بالذكر أنه في WinForms أو أحد تطبيقات WPF ، سيعمل هذا الرمز بشكل صحيح إذا تم استدعاؤه من السلسلة الرئيسية. سيكون هناك سياق التزامن الذي ينفذ العودة إلى موضوع واجهة المستخدم بعد الانتظار. في أي حال ، يجب ألا تلعب مع القسم الحرج في سياق الكود الذي يحتوي على عامل انتظار. في هذه الحالات ، من الأفضل استخدام بدائل التزامن ، والتي ستتم مناقشتها لاحقًا.

عند الحديث عن عمل القسم الحرج في .NET ، تجدر الإشارة إلى ميزة أخرى لتنفيذه. يعمل القسم المهم في .NET في وضعين: وضع الانتظار الدوراني ووضع kernel. يتم تمثيل خوارزمية تدور الانتظار بشكل ملائم على أنها الرمز الكاذب التالي:

 while(!TryEnter(syncObject)) ; 

يهدف هذا التحسين إلى أسرع عملية التقاط للقسم الحرج في وقت قصير ، بناءً على افتراض أنه إذا كان المورد مشغولًا الآن ، فهو على وشك تحرير نفسه. إذا لم يحدث هذا في فترة زمنية قصيرة ، فسيذهب الخيط إلى الانتظار في وضع kernel ، والذي ، مثل العودة منه ، يستغرق وقتًا. قام مطورو .NET بتحسين سيناريو القفل القصير قدر الإمكان ، لسوء الحظ ، إذا بدأت العديد من مؤشرات الترابط بتمزيق القسم الحرج ، فقد يؤدي ذلك إلى تحميل وحدة المعالجة المركزية (CPU) بشكل كبير ومفاجئ.

SpinLock ، SpinWait


منذ أن ذكرت خوارزمية تدور الانتظار ، تجدر الإشارة إلى هياكل BCL SpinLock و SpinWait. يجب استخدامها إذا كان هناك سبب للاعتقاد بأنه ستكون هناك دائمًا فرصة لاتخاذ القفل بسرعة. من ناحية أخرى ، لا يستحق تذكرها قبل أن تظهر نتائج التوصيف أن استخدام بدائل التزامن الأخرى هو عنق الزجاجة لبرنامجك.

Monitor.Wait ، Monitor.Pulse [الكل]


يجب النظر في هذا الزوج من الأساليب معًا. بمساعدتهم ، يمكن تنفيذ العديد من سيناريوهات المنتجين والمستهلكين.

منتج - المستهلك - نمط تصميم متعدد العمليات / متعدد الخيوط بافتراض وجود واحد أو أكثر من سلاسل العمليات / العمليات التي تنتج البيانات وعملية واحدة أو أكثر من العمليات / سلاسل العمليات التي تعالج هذه البيانات. عادة ما تستخدم مجموعة مشتركة.

لا يمكن استدعاء كلتا الطريقتين إلا إذا كان مؤشر الترابط الذي يسببها له قفل في الوقت الحالي. طريقة الانتظار تطلق القفل وتوقف حتى يستجيب مؤشر ترابط آخر للنبض.

لتوضيح العمل ، كتبت مثالًا صغيرًا:

 object syncObject = new object(); Thread t1 = new Thread(T1); t1.Start(); Thread.Sleep(100); Thread t2 = new Thread(T2); t2.Start(); 

(استخدمت الصورة ، وليس النص ، لإظهار ترتيب تنفيذ التعليمات بصريًا)

تحليل: تعيين تأخير 100ms في بداية الدفق الثاني ، وتحديدا لضمان أن يبدأ تنفيذه في وقت لاحق.
- T1: خط تيار # 2 يبدأ
- T1: يدخل الخط رقم 3 إلى القسم الحرج
- T1: الخط رقم 6 يسقط التيار نائما
- T2: يبدأ الخط رقم 3
- T2: يتجمد الخط رقم 4 أثناء انتظار قسم حرج
- T1: الخط رقم 7 يطلق القسم الحرج ويتجمد أثناء انتظار خروج النبض
- T2: يدخل الخط رقم 8 في القسم الحرج
- T2: الخط رقم 11 يخطر T1 باستخدام طريقة النبض
- T2: خط # 14 يخرج من القسم الحرج. حتى ذلك الحين ، لا يمكن T1 متابعة التنفيذ.
- T1: الخط رقم 15 يستيقظ
- T1: السطر رقم 16 يترك القسم الحرج

لدى MSDN ملاحظة مهمة بخصوص استخدام أساليب Pulse / Wait ، وهي: مراقب لا يخزن معلومات الحالة ، مما يعني أنه إذا تم استدعاء طريقة Pulse قبل استدعاء طريقة الانتظار ، فقد يؤدي ذلك إلى توقف تام. إذا كان هذا الموقف ممكنًا ، فمن الأفضل استخدام أحد فئات عائلة ResetEvent.

يوضح المثال السابق بوضوح كيفية عمل أساليب الانتظار / النبض لفئة الشاشة ، لكنه لا يزال يترك أسئلة حول متى يجب استخدامه. مثال جيد على ذلك هو تطبيق BlockingQueue <T> ، من ناحية أخرى ، تطبيق BlockingCollection <T> من System.Collections.Concurrent يستخدم SemaphoreSlim للتزامن.

ReaderWriterLockSlim


هذا هو بلدي التزامن الحبيب البدائية ، ويمثلها فئة System.Threading مساحة الاسم التي تحمل الاسم نفسه. يبدو لي أن العديد من البرامج ستعمل بشكل أفضل إذا استخدم مطوروها هذه الفئة بدلاً من القفل المعتاد.

الفكرة: يمكن قراءة العديد من مؤشرات الترابط ، كتابة واحدة فقط. بمجرد أن يعلن البث عن رغبته في الكتابة ، لا يمكن بدء قراءات جديدة ، ولكن سينتظر اكتمال التسجيل. هناك أيضًا مفهوم قفل القراءة القابل للترقية ، والذي يمكن استخدامه إذا فهمت أثناء عملية القراءة أنك بحاجة إلى كتابة شيء ما ، سيتم تحويل هذا القفل إلى قفل الكتابة في عملية ذرية واحدة.

هناك أيضًا فئة ReadWriteLock في مساحة الاسم System.Threading ، لكن يوصى بشدة بالتطوير الجديد. سيسمح الإصدار النحيف بتجنب عدد من الحالات التي تؤدي إلى طريق مسدود ، إلى جانب أنه يتيح لك التقاط القفل بسرعة ، لأنه يدعم المزامنة في وضع الانتظار الدوراني قبل مغادرته لوضع kernel.

إذا لم تكن قد عرفت وقت كتابة هذا المقال بعد حول هذه الفئة ، فأعتقد الآن أنك استذكرت أمثلة قليلة من الكود المكتوب مؤخرًا ، حيث يتيح هذا النهج للتأمين للبرنامج العمل بكفاءة.

واجهة فئة ReaderWriterLockSlim بسيطة ومباشرة ، لكن لا يمكن وصف استخدامها بسهولة:

 var @lock = new ReaderWriterLockSlim(); @lock.EnterReadLock(); try { // ... } finally { @lock.ExitReadLock(); } 

أرغب في التفاف استخدامه في الفصل ، مما يجعل استخدامه أكثر ملاءمة.
الفكرة: لجعل طرق القراءة / WriteLock التي تعيد كائنًا باستخدام طريقة التخلص ، فإن ذلك سيتيح لهم استخدامها في استخدام وعدد الخطوط التي بالكاد تختلف عن القفل المعتاد.

 class RWLock : IDisposable { public struct WriteLockToken : IDisposable { private readonly ReaderWriterLockSlim @lock; public WriteLockToken(ReaderWriterLockSlim @lock) { this.@lock = @lock; @lock.EnterWriteLock(); } public void Dispose() => @lock.ExitWriteLock(); } public struct ReadLockToken : IDisposable { private readonly ReaderWriterLockSlim @lock; public ReadLockToken(ReaderWriterLockSlim @lock) { this.@lock = @lock; @lock.EnterReadLock(); } public void Dispose() => @lock.ExitReadLock(); } private readonly ReaderWriterLockSlim @lock = new ReaderWriterLockSlim(); public ReadLockToken ReadLock() => new ReadLockToken(@lock); public WriteLockToken WriteLock() => new WriteLockToken(@lock); public void Dispose() => @lock.Dispose(); } 

هذه الخدعة تسمح لك بالكتابة ببساطة:

 var rwLock = new RWLock(); // ... using(rwLock.ReadLock()) { // ... } 


ResetEvent الأسرة


أقوم بتضمين الفئات ManualResetEvent و ManualResetEventSlim و AutoResetEvent لهذه العائلة.
يمكن أن تكون فئات ManualResetEvent وإصدارها سليم وفئة AutoResetEvent في حالتين:
- في cock-cocked (بدون إشارة) ، في هذه الحالة ، تتجمد جميع مؤشرات الترابط التي تسمى WaitOne حتى ينتقل الحدث إلى حالة الإشارة.
- الحالة المنخفضة (المشار إليها) ، في هذه الحالة يتم تحرير جميع التدفقات المعلقة على مكالمة WaitOne. جميع المكالمات WaitOne الجديدة على حدث الجري تمر على الفور بشكل مشروط.

تختلف فئة AutoResetEvent عن فئة ManualResetEvent من حيث أنها تدخل تلقائيًا حالة مُعدلة بعد أن تقوم بإصدار مؤشر ترابط واحد تمامًا. في حالة تعليق عدة مؤشرات ترابط في انتظار AutoResetEvent ، فستصدر المكالمة Set تعسفية واحدة فقط ، على عكس ManualResetEvent. سوف ManualResetEvent الافراج عن جميع المواضيع.

دعونا نلقي نظرة على مثال عن كيفية عمل AutoResetEvent:
 AutoResetEvent evt = new AutoResetEvent(false); Thread t1 = new Thread(T1); t1.Start(); Thread.Sleep(100); Thread t2 = new Thread(T2); t2.Start(); 


يُظهر المثال أن الحدث ينتقل إلى حالة تم ضبطها (بدون إشارة) تلقائيًا فقط عن طريق ترك الخيط المعلقة على استدعاء WaitOne.

فئة ManualResetEvent ، بخلاف ReaderWriterLock ، لم يتم تعليمها على أنها مهجورة ولا يوصى باستخدامها بعد ظهور إصدار Slim الخاص بها. يتم استخدام نسخة ضئيلة من هذه الفئة بكفاءة لتوقعات قصيرة ، كما يحدث ذلك في وضع Spin-Wait ، الإصدار العادي مناسب للإصدارات الطويلة.

بالإضافة إلى فئات ManualResetEvent و AutoResetEvent ، توجد فئة CountdownEvent أيضًا. هذا الفصل مناسب لتنفيذ الخوارزميات ، حيث يتبع الجزء الذي تمكن من الموازاة الجزء الذي يجمع النتائج. يُعرف هذا النهج باسم شوكة الصلة . تم تخصيص مقال ممتاز لعمل هذا الفصل ، وبالتالي لن أقوم بتحليله بالتفصيل هنا.

النتائج


  • عند العمل مع مؤشرات الترابط ، جهازي المشاكل التي تؤدي إلى نتائج غير صحيحة أو مفقودة هي حالة السباق و حالة توقف تام
  • المشاكل التي تسبب البرنامج في قضاء المزيد من الوقت أو الموارد - تجويع الخيط والانتظار مشغول
  • .NET غني في تزامن الصفحات
  • هناك 2 أوضاع الانتظار قفل - سبين الانتظار ، كور الانتظار. استخدام بعض الأولويات المزامنة مؤشر ترابط .NET كليهما
  • Interlocked عبارة عن مجموعة من العمليات الذرية ، وتستخدم في خوارزميات خالية من القفل ، وهي أسرع بدائية التزامن
  • يقوم كل من المشغل lock و Monitor.Enter / Exit بتطبيق فكرة القسم المهم - قطعة من التعليمات البرمجية التي يمكن تنفيذها فقط بواسطة مؤشر ترابط واحد في كل مرة
  • طرق Monitor.Pulse / Wait ملائمة لتنفيذ البرامج النصية الخاصة بالمنتجين والمستهلكين
  • قد يكون ReaderWriterLockSlim أكثر كفاءة من القفل العادي في البرامج النصية حيث تكون القراءة المتوازية مقبولة
  • قد تأتي عائلة الفئة ResetEvent في متناول اليد لمزامنة مؤشر الترابط.

Source: https://habr.com/ru/post/ar459514/


All Articles