
يوفر النظام الأساسي .NET العديد من بدايات التزامن المدمجة ومجموعات سلاسل أمان آمنة. إذا كنت بحاجة إلى تنفيذ ، على سبيل المثال ، ذاكرة تخزين مؤقت آمنة لمؤشر الترابط أو قائمة انتظار طلب عند تطوير تطبيق ما ، عادة ما تستخدم هذه الحلول الجاهزة ، وأحيانًا متعددة في وقت واحد. في بعض الحالات ، يؤدي هذا إلى مشاكل في الأداء: الانتظار الطويل للأقفال ، والاستهلاك المفرط للذاكرة وجمع البيانات المهملة الطويلة.
يمكن حل هذه المشكلات إذا أخذنا في الاعتبار أن الحلول القياسية أصبحت عامة تمامًا - يمكن أن يكون لها تأثير كبير في سيناريوهاتنا التي لا لزوم لها. وفقًا لذلك ، يمكنك أن تكتب ، على سبيل المثال ، مجموعتك الفعالة لسلسلة الرسائل الخاصة بحالة معينة.
تحت cutscene هو مقطع فيديو ونسخة من تقريري من مؤتمر
DotNext ، حيث أقوم بتحليل بعض الأمثلة عند استخدام أدوات من مكتبة .NET القياسية (Task.Delay و SemaphoreSlim و ConcurrentDictionary) أدت إلى انخفاض الأداء ، واقترح حلولاً مصممة خصيصًا لمهام محددة وخالية من هذه العيوب.
في وقت التقرير ، كان يعمل في Kontur. يقوم Kontur بتطوير تطبيقات مختلفة للأعمال ، والفريق الذي عملت فيه يتعامل مع البنية التحتية ويطور خدمات الدعم المختلفة والمكتبات التي تساعد المطورين في الفرق الأخرى على إنشاء خدمات منتجات.
يقوم فريق البنية التحتية ببناء مستودع البيانات الخاص به ، ونظام استضافة التطبيقات لنظام التشغيل Windows والمكتبات المختلفة لتطوير الخدمات المصغرة. تعتمد تطبيقاتنا على بنية الخدمات المصغرة - تتفاعل جميع الخدمات مع بعضها البعض عبر الشبكة ، وبالطبع ، فإنها تستخدم الكثير من التعليمات البرمجية غير المتزامنة والمتعددة الخيوط. بعض هذه التطبيقات لها أهمية بالغة في الأداء ؛ فهي بحاجة إلى أن تكون قادرة على معالجة الكثير من الطلبات.
ما الذي سنتحدث عنه اليوم؟
- تعدد العمليات وتزامن في. NET ؛
- حشو بدائل التزامن والمجموعات ؛
- ماذا تفعل إذا كانت الطرق القياسية لا تستطيع مواجهة الحمل؟
دعنا نحلل بعض ميزات العمل برمز متعدد مؤشرات الترابط وغير متزامن في .NET. دعونا نلقي نظرة على بعض بدائل التزامن والمجموعات المتزامنة ، ونرى كيف يتم ترتيبها بالداخل. سنناقش ما يجب القيام به إذا لم يكن هناك أداء كافٍ ، وإذا لم تتمكن الفئات القياسية من التغلب على العبء ، وما إذا كان يمكن القيام بشيء ما في هذا الموقف.
سأخبرك بأربعة قصص حدثت في موقع الإنتاج لدينا.
التاريخ 1: Task.Delay و TimerQueue
هذه القصة معروفة جيدًا بالفعل ، بما في ذلك حولها في DotNext السابقة. ومع ذلك ، فقد حصلت على تتمة مثيرة للاهتمام إلى حد ما ، لذلك أضفته. إذن ما هي النقطة؟
1.1 الاقتراع والاقتراع الطويل
ينفذ الخادم عمليات طويلة ، وينتظر العميل هذه العمليات.
الاقتراع: يسأل العميل الخادم بشكل دوري عن النتيجة.
استطلاعات طويلة: يرسل العميل طلبًا مع مهلة طويلة ، ويستجيب الخادم عند اكتمال العملية.
المزايا:
- أقل حركة المرور
- يتعرف العميل على النتيجة بشكل أسرع
تخيل أن لدينا خادم يمكنه معالجة بعض الطلبات الطويلة ، على سبيل المثال ، تطبيق يحول ملفات XML إلى PDF ، وهناك عملاء يقومون بتشغيل هذه المهام للمعالجة ويريدون انتظار نتائجهم بشكل غير متزامن. كيف يمكن تحقيق هذا التوقع؟
الطريقة الأولى هي
الاقتراع . يبدأ العميل المهمة على الخادم ، ثم يتحقق بشكل دوري من حالة هذه المهمة ، بينما يعرض الخادم حالة المهمة ("مكتمل" / "فشل" / "مكتمل بخطأ"). يرسل العميل طلبات بشكل دوري حتى تظهر النتيجة.
الطريقة الثانية هي
الاقتراع الطويل . الفرق هنا هو أن العميل يرسل طلبات مع مهلة طويلة. الخادم الذي يتلقى مثل هذا الطلب ، لن يبلغ على الفور بأن المهمة لم تكتمل ، ولكنه سيحاول الانتظار بعض الوقت حتى تظهر النتيجة.
فما هي ميزة الاقتراع الطويل على الاقتراع المنتظم؟ أولاً ، يتم إنشاء حركة مرور أقل. نحن نقدم عددًا أقل من طلبات الشبكة - يتم تعقب حركة مرور أقل عبر الشبكة. أيضًا ، سيتمكن العميل من معرفة النتيجة بشكل أسرع من الاستطلاع المنتظم ، لأنه لا يحتاج إلى انتظار الفاصل الزمني بين العديد من طلبات الاقتراع. ما نريد الحصول عليه أمر مفهوم. كيف سننفذ هذا في الكود؟
المهمة: مهلة
نريد أن ننتظر المهمة مع مهلة
في انتظار SendAsync () ؛
على سبيل المثال ، لدينا مهمة ترسل طلبًا إلى الخادم ، ونريد أن ننتظر نتيجتها مع مهلة ، أي أننا سنرجع نتيجة هذه المهمة أو نرسل نوعًا من الخطأ. سيبدو رمز C # هكذا:
var sendTask = SendAsync(); var delayTask = Task.Delay(timeout); var task = await Task.WhenAny(sendTask, delayTask); if (task == delayTask) return Timeout;
يُطلق هذا الرمز مهمتنا ، التي نريد أن ننتظر نتيجة لها ، و Task.Delay. بعد ذلك ، باستخدام Task.WhenAny ، نحن في انتظار إما Task أو Task.Delay. إذا تبين أن Task.Delay يتم تنفيذه أولاً ، فإن الوقت قد انتهى ولدينا مهلة ، يجب علينا إرجاع خطأ.
هذا الكود ، بالطبع ، ليس مثاليًا ويمكن تحسينه. على سبيل المثال ، لن يضر إلغاء Task.Delay في حالة إرجاع SendAsync مسبقًا ، لكن هذا ليس مثيراً للاهتمام بالنسبة لنا الآن. خلاصة القول هي أنه إذا كتبنا مثل هذا الرمز وقمنا بتطبيقه في عملية الاقتراع الطويلة مع مهلات طويلة ، فسنواجه بعض مشكلات الأداء.
1.2 مشاكل الاقتراع الطويل
- مهلات كبيرة
- العديد من الاستفسارات المتزامنة
- => استخدام وحدة المعالجة المركزية عالية
في هذه الحالة ، تكمن المشكلة في ارتفاع استهلاك موارد المعالج. قد يحدث أن يتم تحميل المعالج بالكامل بنسبة 100٪ ، وأن التطبيق يتوقف عمومًا عن العمل. يبدو أننا لا نستهلك موارد المعالج على الإطلاق: نحن نقوم ببعض العمليات غير المتزامنة ، ننتظر استجابة من الخادم ، ولا يزال المعالج محملاً.
عندما واجهنا هذا الموقف ، أزلنا ملف تفريغ الذاكرة من تطبيقنا:
~*e!clrstack System.Threading.Monitor.Enter(System.Object) System.Threading.TimerQueueTimer.Change(…) System.Threading.Timer.TimerSetup(…) System.Threading.Timer..ctor(…) System.Threading.Tasks.Task.Delay(…)
لتحليل التفريغ ، استخدمنا أداة WinDbg. لقد أدخلنا أمرًا يُظهر تتبعات المكدس لجميع مؤشرات الترابط المُدارة ، ورأينا هذه النتيجة. لدينا الكثير من الخيوط في العملية التي تنتظر بعض القفل. طريقة Monitor.Enter هي ما يوسع إليه بناء القفل في C #. يتم التقاط هذا القفل داخل فصول تسمى Timer و TimerQueueTimer. في Timer ، لقد أتينا من Task.Delay عندما حاولنا إنشائها. ما هذا؟ عند بدء تشغيل Task.Delay ، يتم التقاط القفل داخل TimerQueue.
1.3 قفل القافلة
- العديد من المواضيع محاولة قفل قفل واحد
- تحت القفل ، يتم تنفيذ رمز صغير
- يقضي الوقت في تزامن الخيط ، وليس تنفيذ التعليمات البرمجية.
- يتم حظر Threadblocks - فهي ليست لانهائية
كان لدينا قافلة قفل في التطبيق. تحاول العديد من مؤشرات الترابط التقاط نفس القفل. تحت هذا القفل ، يتم تنفيذ الكثير من التعليمات البرمجية. لا يتم إنفاق موارد المعالج هنا على رمز التطبيق نفسه ، ولكن على عمليات مزامنة مؤشرات الترابط فيما بينها على هذا القفل. تجدر الإشارة أيضًا إلى ميزة مرتبطة بـ .NET: مؤشرات الترابط التي تشارك في قافلة القفل هي سلاسل مؤشرات من تجمع مؤشرات الترابط.
وفقًا لذلك ، إذا تم حظر مؤشرات الترابط من تجمع مؤشرات الترابط ، فقد تنتهي - يكون عدد مؤشرات الترابط في تجمع مؤشرات الترابط محدودًا. يمكن تهيئته ، لكن لا يزال هناك حد أعلى. بعد الوصول إليه ، ستشارك جميع مؤشرات ترابط threadpool في قافلة القفل ، وأي كود ينطوي على threadpool سيتوقف تنفيذه في التطبيق. هذا يفاقم الوضع إلى حد كبير.
1.4 TimerQueue
- يدير الموقتات في تطبيق .NET.
- تستخدم أجهزة ضبط الوقت في:
- Task.Delay
- الإلغاء. إلغاء
- المتشعب
TimerQueue هي فئة تدير جميع أجهزة ضبط الوقت في تطبيق .NET. إذا قمت بالبرمجة مرة واحدة في WinForms ، فقد تكون قمت بإنشاء أجهزة ضبط الوقت يدويًا. بالنسبة لأولئك الذين لا يعرفون موقتات الوقت: يتم استخدامها في Task.Delay (هذه هي حالتنا فقط) ، كما يتم استخدامها داخل CancellationToken ، في أسلوب CancelAfter. وهذا يعني أن استبدال Task.Delay بـ CancellationToken.CancelAfter لن يساعدنا بأي طريقة. بالإضافة إلى ذلك ، يتم استخدام أجهزة ضبط الوقت في العديد من فئات .NET الداخلية ، على سبيل المثال ، في HttpClient.
بقدر ما أعرف ، فإن بعض تطبيقات معالجات HttpClient لها أجهزة ضبط الوقت. حتى إذا لم تستخدمها بشكل صريح ، فلا تبدأ تشغيل Task.Delay ، على الأرجح ، لا تزال تستخدمها على أي حال.
الآن دعونا نلقي نظرة على كيفية ترتيب TimerQueue بالداخل.
- الحالة العالمية (لكل نطاق):
- قائمة مزدوجة مرتبطة TimerQueueTimer
- قفل الكائن - الموقتات الروتينية Callbacks
- مؤقتات غير مرتبة حسب وقت الاستجابة
- إضافة مؤقت: O (1) + قفل
- إزالة الموقت: O (1) + قفل
- بدء توقيت: O (N) + قفل
يوجد داخل TimerQueue حالة عمومية ، وهي قائمة مرتبطة بشكل مضاعف بالكائنات من النوع TimerQueueTimer. يحتوي TimerQueueTimer على رابط إلى TimerQueueTimer آخر ، مجاور في قائمة مرتبطة ، كما أنه يحتوي على وقت المؤقت وإعادة الاتصال ، والذي سيتم استدعاؤه عند إطلاق المؤقت. هذه القائمة المرتبطة مضاعفة محمية بواسطة كائن قفل ، فقط تلك التي وقعت قافلة القفل في طلبنا. أيضًا داخل TimerQueue ، هناك روتين يُطلق عمليات رد اتصال مرتبطة بأجهزة ضبط الوقت لدينا.
لا يتم ترتيب الموقتات بأي حال من الأحوال بوقت الاستجابة ، حيث يتم تحسين الهيكل بالكامل لإضافة / إزالة مؤقتات جديدة. عند بدء تشغيل Routine ، يتم تشغيله من خلال القائمة ذات الارتباط المزدوج بالكامل ، وتحديد أجهزة ضبط الوقت التي يجب أن تعمل ، وإعادة الاتصال بهم.
تعقيد العملية هنا هو هكذا. إضافة وإزالة موقت يحدث O لكل وحدة ، ويحدث بداية الموقتات في كل سطر. علاوة على ذلك ، إذا كان كل شيء مقبولًا مع التعقيد الخوارزمي ، فهناك مشكلة واحدة: كل هذه العمليات تلتقط القفل ، وهو أمر غير جيد جدًا.
ما الوضع يمكن أن يحدث؟ لدينا الكثير من أجهزة ضبط الوقت المتراكمة في TimerQueue ، لذلك عندما يبدأ Routine ، فإنه يقوم بإغلاق التشغيل الخطي الطويل ، في ذلك الوقت ، أولئك الذين يحاولون بدء أو إزالة أجهزة ضبط الوقت من TimerQueue لا يمكنهم فعل أي شيء حيال ذلك. وبسبب هذا ، يحدث قافلة القفل. تم إصلاح هذه المشكلة في .NET Core.
تقليل التنازع قفل الموقت (coreclr # 14527)
- قفل تقاسم
- Environment.ProcessorCount TimerQueue's TimerQueueTimer - طوابير منفصلة للوقت قصير / طويل الأجل
- مؤقت قصير: الوقت <= 1/3 ثانية
https://github.com/dotnet/coreclr/issues/14462
https://github.com/dotnet/coreclr/pull/14527
كيف تم إصلاحها؟ لقد داهموا TimerQueue: بدلاً من TimerQueue واحد ، والذي كان ثابتًا لكل AppDomain بالكامل ، للتطبيق بأكمله ، تم إنشاء TimerQueue عدة. عند وصول مؤشرات الترابط إلى هناك ومحاولة بدء أجهزة ضبط الوقت الخاصة بهم ، ستقع هذه أجهزة ضبط الوقت في TimerQueue عشوائي ، وستكون فرص سلاسل العمل أقل من الاصطدام على قفل واحد.
أيضا في. NET الأساسية تطبيق بعض التحسينات. تم تقسيم أجهزة ضبط الوقت إلى وقت طويل وقصير الأجل ، وتستخدم الآن TimerQueue منفصلة بالنسبة لهم. يتم تحديد المؤقت قصير الأجل ليكون أقل من 1/3 من الثانية. لا أعرف لماذا تم اختيار مثل هذا الثابت. في .NET Core ، فشلنا في التعرف على مشاكل أجهزة ضبط الوقت.
https://github.com/Microsoft/dotnet-framework-early-access/blob/master/release-notes/NET48/dotnet-48-changes.mdhttps://github.com/dotnet/coreclr/labels/netfx-port-considerهذا الإصلاح تم ترحيله إلى .NET Framework ، الإصدار 4.8. يشار إلى علامة netfx-port -نظر في الرابط أعلاه ، إذا انتقلت إلى مستودع .NET Core ، و CoreCLR ، و CoreFX ، يمكنك البحث عن هذه المشكلة التي سيتم ترحيلها إلى .NET Framework ، وهناك الآن حوالي خمسين منها. وهذا هو ، المصدر المفتوح. NET ساعد كثيرا ، تم إصلاح عدد غير قليل من الأخطاء. يمكنك قراءة changelog .NET Framework 4.8: تم إصلاح الكثير من الأخطاء ، أكثر بكثير من إصدارات .NET الأخرى. ومن المثير للاهتمام ، أن هذا الإصلاح متوقف عن التشغيل بشكل افتراضي في .NET Framework 4.8. يتم تضمينه في الملف بأكمله الذي تعرفه يسمى App.config
يسمى الإعداد في App.config الذي يمكّن هذا الإصلاح UseNetCoreTimer. قبل أن يتم إصدار .NET Framework 4.8 ، لكي يعمل تطبيقنا ولا تدخل في قافلة القفل ، كان عليك استخدام تطبيق Task.Delay. في ذلك ، حاولنا استخدام كومة ثنائية من أجل فهم أكثر فاعلية الموقتات التي يجب استدعاء الآن.
1.5 Task.Delay: التنفيذ الأصلي
- BinaryHeap
- عملية التجزئة
- لقد ساعد ، ولكن ليس في جميع الحالات
يتيح لك استخدام كومة ثنائية ثنائية تحسين "الروتين" ، الذي يستدعي عمليات الاسترجاعات ، ولكنه يزيد من الوقت الذي يستغرقه إزالة مؤقت تعسفي من قائمة الانتظار - لهذا تحتاج إلى إعادة إنشاء الكومة. هذا على الأرجح السبب وراء استخدام .NET لقائمة مزدوجة الارتباط. بالطبع ، مجرد استخدام كومة ثنائية لن يساعدنا هنا ، كان علينا أيضًا أن نفحص TimerQueue. نجح هذا الحل لبعض الوقت ، ولكن بعد كل ذلك ، وقع كل شيء مرة أخرى في قافلة القفل نظرًا لحقيقة أن أجهزة ضبط الوقت تستخدم ليس فقط حيث يتم تشغيلها في التعليمات البرمجية بشكل صريح ، ولكن أيضًا في مكتبات الأطراف الثالثة ورمز .NET. لإصلاح هذه المشكلة تمامًا ، يجب الترقية إلى الإصدار .NET Framework 4.8 وتمكين الإصلاح من مطوري .NET.
1.6 Task.Delay: الاستنتاجات
- مطبات في كل مكان - حتى في الأشياء الأكثر استخداما
- هل اختبار الإجهاد
- قم بالتبديل إلى Core ، واحصل على إصلاحات الأخطاء (والأخطاء الجديدة) أولاً :)
ما هي الاستنتاجات من هذه القصة كلها؟ أولاً ، يمكن العثور على المزالق في كل مكان حقًا ، حتى في الفصول الدراسية التي تستخدمها يوميًا دون التفكير ، على سبيل المثال ، نفس المهمة ، Task.Delay.
أوصي بإجراء اختبار الإجهاد لمقترحاتك. هذه المشكلة التي حددناها للتو في مرحلة اختبار الحمل. ثم أطلقناها عدة مرات على الإنتاج في تطبيقات أخرى ، لكن مع ذلك ، ساعدنا اختبار الإجهاد على تأخير الوقت قبل أن نواجه هذه المشكلة في الواقع.
قم بالتبديل إلى .NET Core - ستكون أول من يتلقى إصلاحات الأخطاء (والأخطاء الجديدة). أين بدون أخطاء جديدة؟
انتهت قصة أجهزة ضبط الوقت ، وننتقل إلى المرحلة التالية.
القصة 2: سيمافور سليم
القصة التالية تدور حول SemaphoreSlim المعروفة.
2.1 خادم الاختناق
- يجب تحديد عدد الطلبات التي تتم معالجتها بشكل متزامن على الخادم
أردنا تنفيذ الاختناق على الخادم. ما هذا ربما تعرف جميعًا الاختناق على وحدة المعالجة المركزية: عندما يسخن المعالج ، فإنه يقلل من تردده ليبرد ، وهذا يحد من أدائه. لذلك هو هنا. نحن نعلم أن خادمنا يمكنه معالجة طلبات N بالتوازي وليس السقوط. ماذا نريد ان نفعل؟ حدد عدد الطلبات التي تمت معالجتها في وقت واحد بهذا الثابت واتركها بحيث إذا ظهرت المزيد من الطلبات ، فإنها تنتظر وتنتظر حتى يتم تنفيذ الطلبات التي جاءت في وقت سابق. كيف يمكن حل هذه المشكلة؟ من الضروري استخدام نوع من التزامن البدائي.
Semaphore هي بدائية التزامن حيث يمكنك الانتظار N Times ، وبعدها الشخص الذي يصل N + أولاً وما إلى ذلك سوف ينتظرها حتى يصدر أولئك الذين دخلوها سابقًا Semaphore. اتضح شيء من هذا القبيل: خيطان من الإعدام ، وذهب عاملان تحت سيمافور ، والباقي في الصف.

بطبيعة الحال ، فإن Semaphore لا يناسبنا تمامًا ، فهو في .NET متزامن ، لذلك أخذنا SemaphoreSlim وكتبنا هذا الرمز:
var semaphore = new SemaphoreSlim(N); … await semaphore.WaitAsync(); await HandleRequestAsync(request); semaphore.Release();
ننشئ SemaphoreSlim ، انتظر ، تحت Semaphore نقوم بمعالجة طلبك ، بعد أن نصدر Semaphore. يبدو أن هذا تطبيق مثالي لخنق الخادم ، ولم يعد من الممكن أن يكون أفضل. لكن كل شيء أكثر تعقيدًا.
2.2 خادم الاختناق: المضاعفات
- معالجة الطلبات بترتيب LIFO
- SemaphoreSlim
- ConcurrentStack
- TaskCompletionSource
نسينا قليلا عن منطق العمل. الطلبات التي تأتي إلى الاختناق هي طلبات http حقيقية. كقاعدة عامة ، لديهم بعض المهلة ، التي يتم تعيينها من قبل أولئك الذين أرسلوا هذا الطلب تلقائيًا ، أو مهلة للمستخدم الذي يضغط F5 بعد بعض الوقت. وفقًا لذلك ، إذا قمت بمعالجة الطلبات في ترتيب قائمة انتظار ، مثل إشارة منتظمة ، فربما يتم أولاً معالجة جميع الطلبات المقدمة من قائمة الانتظار التي انقضت مهلتها بالفعل. إذا كنت تعمل بترتيب مكدس - قم أولاً بمعالجة جميع الطلبات التي جاءت الأخيرة ، لن تنشأ مثل هذه المشكلة.
بالإضافة إلى SemaphoreSlim ، كان علينا استخدام ConcurrentStack ، TaskCompletionSource ، لربط الكثير من التعليمات البرمجية حول كل هذا ، بحيث كان كل شيء يعمل بالترتيب الذي نحتاجه. TaskCompletionSource شيء من هذا القبيل ، والذي يشبه CancellationTokenSource ، ولكن ليس لـ CancellationToken ، ولكن لـ Task. يمكنك إنشاء TaskCompletionSource ، وسحب مهمة منها ، وإخراجه ، ثم إخبار TaskCompletionSource أنك بحاجة إلى تعيين النتيجة لهذه المهمة ، وسيعرف أولئك الذين ينتظرون هذه المهمة هذه النتيجة.
لقد نفذناها جميعًا. الكود فظيع. والأسوأ من ذلك كله ، اتضح أنها غير صالحة للعمل.
بعد بضعة أشهر من بدء استخدامه في تطبيق محمّل إلى حد كبير ، واجهنا مشكلة. بنفس الطريقة كما في الحالة السابقة ، زاد استهلاك وحدة المعالجة المركزية إلى 100٪. فعلنا نفس الشيء ، أزلنا التفريغ ، ونظرنا إليه في WinDbg ، ووجدنا مرة أخرى قافلة القفل.

هذه المرة وقعت قافلة لوك داخل SemaphoreSlim.WaitAsync و SemaphoreSlim.Release. اتضح أن هناك قفل داخل SemaphoreSlim ، أنها ليست خالية من قفل. تحول هذا إلى عيب خطير بالنسبة لنا.

داخل SemaphoreSlim هناك حالة داخلية (عداد لعدد العمال الذين لا يزال بإمكانهم خوضها) ، وقائمة مضاعفة مرتبطة بأولئك الذين ينتظرون في هذا Semaphore. الأفكار هنا هي نفسها: يمكنك الانتظار في هذا Semaphore ، يمكنك إلغاء توقعك - لترك قائمة الانتظار هذه. هناك قفل دمر حياتنا للتو.
قررنا: أسفل مع كل رمز رهيب كان علينا أن نكتب.

دعنا نكتب إشارة لدينا ، والتي ستكون على الفور خالية من القفل والتي ستعمل على الفور في ترتيب مكدس. إلغاء الانتظار ليس مهمًا بالنسبة لنا.

تحديد هذا الشرط. هنا سيكون الرقم الحاليالعدد - وهذا هو عدد الأماكن المتبقية في إشارة. إذا لم تكن هناك مقاعد متبقية في سيمافور ، فسيكون هذا العدد سالبًا وسيُظهر عدد العمال في قائمة الانتظار. سيكون هناك أيضًا ConcurrentStack ، يتكون من TaskCompletionSource'ov - هذا مجرد كومة من waiter'ov سيتم سحبها منها إذا لزم الأمر. دعنا نكتب طريقة WaitAsync.
var decrementedCount = Interlocked.Decrement(ref currentCount); if (decrementedCount >= 0) return Task.CompletedTask; var waiter = new TaskCompletionSource<bool>(); waiters.Push(waiter); return waiter.Task;
أولاً ، نخفف العداد ، ونأخذ مكانًا واحدًا في سيمافور لأنفسنا ، إذا كان لدينا أماكن مجانية ، ثم نقول: "هذا كل شيء ، لقد ذهبت تحت سيمافور".
إذا لم تكن هناك أماكن في Semaphore ، فسنقوم بإنشاء TaskCompletionSource ، ورميها على كومة من waiter'ov وإرجاع Task إلى العالم الخارجي. عندما يحين الوقت ، ستنجح هذه المهمة ، وسيكون العامل قادرًا على مواصلة عمله وسيخضع لسيطرة سيمافور.
الآن دعونا نكتب طريقة الإصدار.
var countBefore = Interlocked.Increment(ref currentCount) - 1; if (countBefore < 0) { if (waiters.TryPop(out var waiter)) waiter.TrySetResult(true); }
طريقة الإصدار هي كما يلي:
- مقعد واحد مجانا في إشارة
- الزيادة الحاليةالعدد
إذا استطعنا أن نقول بواسطة currentCount ما إذا كان هناك نادل داخل المكدس نحتاج إلى الإشارة إليه ، فنحن نخرج هذا النادل من المكدس والإشارة. هنا النادل هو TaskCompletionSource. السؤال على هذا الكود: يبدو منطقيًا ، لكن هل ينجح؟ ما هي المشاكل هناك؟ هناك فارق بسيط يتعلق بالمكان الذي يتم فيه إطلاق استمرار و TaskCompletionSource'y.

النظر في هذا الرمز. أنشأنا TaskCompletionSource وأطلقت اثنين من المهام. المهمة الأولى تعرض وحدة ، وتعيين النتيجة على TaskCompletionSource ، ثم تعرض الشيطان على وحدة التحكم. تنتظر المهمة الثانية على TaskCompletionSource هذا ، وفي المهمة الخاصة بها ، ثم تقوم دائمًا بحظر مؤشر الترابط من تجمع مؤشرات الترابط.
ماذا سيحدث هنا؟ سيتم تقسيم المهمة 2 عند التحويل البرمجي إلى طريقتين ، والثاني هو استمرار يحتوي على Thread.Sleep. بعد تعيين نتيجة TaskCompletionSource ، سيتم تنفيذ هذه المتابعة في نفس مؤشر الترابط الذي تم فيه تنفيذ المهمة الأولى. وفقًا لذلك ، سيتم حظر تدفق المهمة الأولى إلى الأبد ، ولن تتم طباعة الشرح إلى وحدة التحكم.
ومن المثير للاهتمام ، حاولت تغيير هذا الرمز ، وإذا قمت بإزالة الإخراج إلى وحدة التحكم ، فسيتم بدء المتابعة على مؤشر ترابط آخر من تجمع مؤشرات الترابط وتم طباعة الشرح. في أي من الحالات ، سيتم تنفيذ المتابعة في نفس سلسلة الرسائل ، وفيها - ستصل إلى مجموعة مؤشرات الترابط - سؤال للقراء.
var tcs = new TaskCompletionSource<bool>( TaskCreationOptions.RunContinuationsAsynchronously); Task.Run(() => tcs.TrySetResult(true));
لحل هذه المشكلة ، يمكننا إما إنشاء TaskCompletionSource مع علامة RunContinuationsAsynchronously المقابلة أو استدعاء الأسلوب TrySetResult داخل Task.Run/ThreadPool.QueueUserWorkItem بحيث لا يعمل على مؤشر الترابط الخاص بنا. إذا تم تنفيذه على خيطنا ، فقد يكون لدينا آثار جانبية غير مرغوب فيها. بالإضافة إلى ذلك ، هناك مشكلة ثانية ، سنناقشها بمزيد من التفصيل.

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

يرجع ذلك إلى حقيقة أنه في أسلوب WaitAsync ، فإن تغيير الحالة ليس ذريًا. أولاً نقوم بتقليل العداد وبعد ذلك فقط ندفع النادل إلى المكدس. إذا حدث ذلك ، يتم تنفيذ الإصدار بين التراجع والدفع ، فقد يخرج حتى لا يسحب أي شيء من المكدس. يجب أن يؤخذ ذلك في الاعتبار ، وفي طريقة الإصدار ، انتظر حتى يظهر النادل على المكدس.
var countBefore = Interlocked.Increment(ref currentCount) - 1; if (countBefore < 0) { Waiter waiter; var spinner = new SpinWait(); while (!waiter.TryPop(out waiter)) spinner.SpinOnce(); waiter.TrySetResult(true); }
نحن هنا نفعل ذلك في حلقة حتى نتمكن من سحبها. لكي لا نضيع دورات المعالج مرة أخرى ، نستخدم SpinWait.
في التكرارات القليلة الأولى ، سوف تدور في حلقة. إذا كان هناك الكثير من التكرارات ، فلن يظهر النادل لفترة طويلة ، ثم ينتقل خيطنا إلى Thread.Sleep ، حتى لا نضيع موارد وحدة المعالجة المركزية مرة أخرى.
في الواقع ، LIFO-order Semaphore ليس فقط فكرتنا.
LowLevelLifoSemaphore
- متزامن
- على Windows ، يستخدم منفذ إكمال IO كومة Windows
https://github.com/dotnet/corert/blob/master/src/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs
يوجد مثل هذا الإشارة في .NET نفسه ، ولكن ليس في CoreCLR ، وليس في CoreFX ، ولكن في CoreRT. من المفيد في بعض الأحيان إلقاء نظرة خاطفة على مستودع .NET. هناك إشارة تسمى LowLevelLifoSemaphore. لن يناسبنا هذا الإشارة على أي حال: إنه متزامن.
بشكل ملحوظ ، على ويندوز يعمل من خلال منافذ إكمال IO. لديهم خاصية يمكن أن تنتظرها مؤشرات الترابط ، وسيتم إصدار هذه الخيوط بترتيب LIFO فقط. يتم استخدام هذه الميزة هناك ، إنها منخفضة المستوى حقًا.
2.3 الاستنتاجات:
- لا تأمل في أن يظل ملء الإطار قيد الحمل
- من الأسهل حل مشكلة معينة من الحالة العامة.
- اختبار الإجهاد لا يساعد دائما
- حذار من الحجب
ما هي الاستنتاجات من هذه القصة كلها؟ بادئ ذي بدء ، لا تأمل أن بعض الفئات من الإطار الذي تستخدمه من المكتبة القياسية ستتعامل مع حملك. لا أريد أن أقول إن SemaphoreSlim أمر سيئ ، فقد اتضح أنه غير مناسب على وجه التحديد في هذا السيناريو.
لقد أصبح الأمر أسهل بالنسبة لنا لكتابة إشارة لدينا لمهمة محددة. على سبيل المثال ، لا يدعم إلغاء الانتظار. تتوفر هذه الميزة في SemaphoreSlim المعتادة ، ونحن لا نملكها ، لكن هذا سمح لنا بتبسيط الكود.
اختبار الحمل ، على الرغم من أنه يساعد ، قد لا يساعد دائمًا.
يُعرف .NET بأنه غالبًا ما يتم تأمينه في أماكن غير متوقعة - من الأفضل الحذر منها. إذا قمت بكتابة بنية القفل في التعليمات البرمجية الخاصة بك ، فمن الأفضل أن تفكر: "ما هو الحمل الحقيقي هنا؟" وإذا كان استهلاك وحدة المعالجة المركزية فجأة 100٪ ، فستجد جميع مؤشرات الترابط في وضع القفل ، وربما يحدث هذا في مكان ما داخل .NET. فقط ضع ذلك في الاعتبار.دعنا ننتقل إلى القصة القادمة.القصة 3: (أ) مزامنة IO
/, .

lock convoy, stack trace Overlapped PinnableBufferCache. lock. : Overlapped PinnableBufferCache?
OVERLAPPED — Windows, /. , . , . , lock convoy. , lock convoy, , .

, , .NET 4.5.1 4.5.2. .NET 4.5.2, , .NET 4.5.2. .NET 4.5.1 OverlappedDataCache, Overlapped — , , . , lock-free, ConcurrentStack, . .NET 4.5.2 : OverlappedDataCache PinnableBufferCache.
? PinnableBufferCache , Overlapped , , — . , , . PinnableBufferCache . , lock-free, ConcurrentStack. , . , , - lock-free list lock'.
3.1 PinnableBufferCache
LockConvoy:
lock convoy , - . list , lock , , .
PinnableBufferCache , . :
PinnableBufferCache_System.ThreadingOverlappedData_MinCount
, . : « ! - ». -:
Environment.SetEnvironmentVariable( "PinnableBufferCache_System.Threading.OverlappedData_MinCount", "10000"); new Overlapped().GetHashCode(); for (int i = 0; i < 3; i++) GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
? , Overlapped , , . , , , , PinnableBufferCache lock convoy'. , .
.NET Core PinnableBufferCache
, OverlappedData . , , Garbage collector , . .NET Core . .NET Framework, , .
3.2 :
, . , .NET , . , , .NET Core. , , -.
key-value .
4: Concurrent key-value collections
.NET concurrent-. lock-free ConcurrentStack ConcurrentQueu, . ConcurrentDictionary, . lock-free , , . ConcurrentDictionary?
4.1 ConcurrentDictionary
:
الايجابيات:
- (TryAdd/TryUpdate/AddOrUpdate)
- Lock-free
- Lock-free enumeration
, memory-, , . , , .NET Framework. . , , (enumeration) lock-free. , .
, , - .NET. key-value - :

-, bucket'. bucket', . , bucket , .
— , ConcurrentDictionary. ConcurrentDictionary «-» . , , , memory traffic. ConcurrentDictionary, lock'. — .
, Dictionary.

Dictionary , Concurrent, . : buckets, entries. buckets bucket' entries. «-» entries. . «-» int, bucket'.
memory overhead, ConcurrentDictionary Dictionary.

Dictionary. Memory overhea' , . Dictionary overhead - , int'. 8 .
ConcurrentDictionary. ConcurrentDictionary ConcurrentDictionary.Node. , . int hashCode . , table ( 16 ), int hashCode . , 64- 28 overhead'. Dictionary.
memory overhead', ConcurrentDictionary GC , . Benchmark. ConcurrentDictionary , GC.Collect. ?

. ConcurrentDictionary 10 , , , . Dictionary . , , , . .
, ConcurrentDictionary?
4.2
- TTL
- Dictionary+lock
- Sharding
. ConcurrentDictionary. 10 . , . TTL , . Dictionary lock'. , , lock . Dictionary lock' , - , lock. , .
4.3
- in-memory <Guid,Guid>
- >10 6
. — , in-memory Guid' Guid, . . - - , . , 15 . . Semaphore ConcurrentDictionary.

, lock-free , overhead GC. , . , , , . , - , , . , , Large Object Heap. ?
, , Dictionary .

Dictionary bucket', Entry. Entry , , , .

Dictionary , , . , - .
, - ? -, , , , . . Dictionary, , buckets, entries, Interlocked. , .
Dictionary
- ,
- , ?
— Resize buckets entries
— -
— Dictionary.Entry
— -
https://blogs.msdn.microsoft.com/tess/2009/12/21/high-cpu-in-net-app-using-a-static-generic-dictionary/
, Dictionary - bucket'. , . , , . , , .
Entry Dictionary. - - . , .

.NET Framework 1.1. Hashtable, Dictionary, object'. MSDN , . , -. . , Hashtable . , .
4.4 Dictionary.Entry

? Dictionary.Entry , , 8 , , , , . ?
bool writing; int version; this.writing = true; buckets[index] = …; this.version++; this.writing = false;
: ( , ) int-. , . , , , , .
bool writing; int version; while (true) { int version = this.version; bucket = bickets[index]; if (this.writing || version != this.version) continue; break; }
, , . , . , 8 .
4.5 -
, .

Dictionary bucket , .
Dictionary, . : 0 2. bucket, 1 2. ? 0. , , 2. . , 2, , , 1. 1 2 — bucket. , , . 1 — , bucket. Hashtable , bucket' -. —
double hashing .
4.6
. , Buckets, Entries ( Buckets, Entries). - , , , , .
. , .
: , , , , . , , .

, , — .
? , - 2. - Capacity , . — 2. , . 2. ? , , , . - , , 3. , , , , , .
, Hashtable, . , double hashing. , , , .
, , — , . Hashtable. , — — . . , bucket', - , . .
, , lock-free LOH.

lock-free ? MSDN Hashtable , . , , .

, , , bucket'. Dictionary bucket', -, bucket' . - bucket, bucket . , .
, Large Object Heap.

. CustomDictionary CustomDictionarySegment . Dictionary, , . — Dictionary, . , Large Object Heap. , bucket' . , , , bucket, - - .
. ConcurrentDictionary, .NET, , .
4.7
? .NET . . , , . - — - . , , , .
- , , , , . , , , , , . — , , .
روابط مفيدة
— ConcurrentDictionary. , , (
Diafilm ), .
GitHub. — , , LIFO-Semaphore, . , .
6-7 DotNext 2019 Moscow «.NET: » , .NET Framework .NET Core, , .