. NET - أدوات للعمل مع تعدد العمليات وتزامن - الجزء 2

لقد نشرت هذه المقالة في الأصل في مدونة CodingSight .
كما أنه متاح باللغة الروسية هنا .

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

محتويات




بشأن الموارد المشتركة


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

مثال رقم 1:

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

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

مثال رقم 2:

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

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

مثال رقم 3:

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

لإبراز العشرات من المسامير النهائية في نعش هذه الفكرة: لا يزال يُعتبر وقت تشغيل واحد و Garbage Collector ، وجدولة مؤشر ترابط واحد ، وذاكرة الوصول العشوائي الموحدة الموحدة فعليًا ، ومعالجًا واحدًا موارد مشتركة.

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

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


يمكننا تصنيف أخطاء البرامج إلى الفئات التالية:
  1. البرنامج لا ينتج نتيجة - إنه يتعطل أو يتجمد.
  2. يعطي البرنامج نتيجة غير صحيحة.
  3. ينتج البرنامج نتيجة صحيحة ولكنه لا يلبي بعض المتطلبات غير المتعلقة بالوظيفة - فهو يقضي الكثير من الوقت أو الموارد.

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


مأزق


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



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

حالة السباق


Race-Condition هو موقف عندما يعتمد كل من سلوك ونتائج العمليات الحسابية على جدولة مؤشر ترابط بيئة التنفيذ

المشكلة هي أن برنامجك يمكن أن يعمل بشكل غير صحيح مرة واحدة في المائة ، أو حتى في المليون.

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

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

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


مشكلة Busy Wait هي مشكلة تحدث عندما ينفق البرنامج موارد المعالج على الانتظار بدلاً من الحساب.

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

while(!hasSomethingHappened) ; 

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

ربما ، يقترح بعض القراء حل مشكلة جوهر واحد يتم شغلها بالكامل مع الانتظار عن طريق إضافة Thread.Sleep (1) (أو شيء مشابه) في الدورة. في حين أنه سيؤدي إلى حل هذه المشكلة ، سيتم إنشاء مشكلة جديدة - الوقت المستغرق للرد على التغييرات سيكون 0.5 مللي ثانية في المتوسط. من ناحية ، ليس هذا كثيرًا ، ولكن من ناحية أخرى ، هذه القيمة أعلى كارثية مما يمكننا تحقيقه باستخدام بدائل التزامن لعائلة 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; 

الفرق الوحيد هو أن الطبقة المتشابكة تنفذ هذا بطريقة ذرية. لذلك ، إذا كتبنا هذا الرمز بأنفسنا ، فقد نواجه سيناريو تم فيه استيفاء شرط الموقع 1 == المقارنة بالفعل. ولكن عندما يتم تنفيذ العبارة location1 = value ، فقد قام مؤشر ترابط مختلف بتغيير قيمة location1 بالفعل ، لذلك سيتم فقدها.

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

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

 class MyClass { public event EventHandler MyEvent; } 

الآن ، دعونا نبني المشروع في تكوين الإصدار وفتح الإنشاء من خلال dotPeek مع تمكين خيار "إظهار رمز المترجم الذي تم إنشاؤه":

 [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 لا يزال كما كان في الوقت الحالي ، بدأنا في تنفيذ Delegate.Combine ، ثم اضبطه على ما يعود عليه Delegate.Combine. إذا لم يكن الأمر كذلك ، فحاول مرة أخرى حتى تعمل.

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

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 في وضعين: الانتظار الدوراني والانتظار الأساسي. يمكننا تمثيل خوارزمية الانتظار الدوراني مثل الكود الكاذب التالي:

 while(!TryEnter(syncObject)) ; 

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

SpinLock ، SpinWait


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

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(); 

(استخدمت صورة بدلاً من نص هنا لإظهار ترتيب تنفيذ التعليمات بدقة)
Explanation: قمت بتحديد زمن انتقال قدره 100 مللي ثانية عند بدء تشغيل الخيط الثاني للتأكد من أنه سيتم تنفيذه فيما بعد.
- T1: السطر # 2 يبدأ الخيط
- T1: السطر رقم 3 يدخل الخيط إلى قسم مهم
- T1: السطر رقم 6 يذهب الخيط إلى النوم
- T2: السطر # 3 يبدأ الخيط
- T2: الخط # 4 يتجمد وينتظر القسم الحرج
- T1: الخط رقم 7 ، يتيح للقسم الحرج أن يتجمد ويتجمد أثناء انتظار خروج النبض
- T2: الخط رقم 8 يدخل القسم الحرج
- T2: الخط # 11 يشير إلى T1 بمساعدة Pulse
- T2: الخط رقم 14 يخرج من القسم الحرج. لا يمكن لـ T1 متابعة تنفيذه قبل حدوث ذلك.
- T1: الخط رقم 15 يخرج من الانتظار
- T1: خط # 16 يخرج من القسم الحرج

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

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

ReaderWriterLockSlim


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

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

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

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

واجهة فئة 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 في حالتين:

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

يختلف AutoResetEvent عن ManualResetEvent لأنه ينتقل تلقائيًا إلى الحالة غير المشار إليها بعد تحرير مؤشر ترابط واحد تمامًا . إذا تم تجميد بعض مؤشرات الترابط أثناء انتظار AutoResetEvent ، فلن يقوم استدعاء Set إلا بإصدار مؤشر ترابط عشوائي واحد ، بدلاً من 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.

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

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

الاستنتاجات


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

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


All Articles