يوم جيد. هذه المرة ، دعنا نتحدث عن موضوع بدأ كل أتباع يحترمون أنفسهم للغة C # في فهمه - البرمجة غير المتزامنة باستخدام Task أو ، في حالة عامة الناس ، غير متزامنة / في انتظار. لقد قامت Microsoft بعمل جيد - من أجل استخدام عدم التزامن في معظم الحالات ، ما عليك سوى معرفة بناء الجملة وأي تفاصيل أخرى. لكن إذا تعمقت ، فإن الموضوع ضخم ومعقد للغاية. صرح بذلك الكثير ، كل في أسلوبه الخاص. هناك الكثير من المقالات الرائعة حول هذا الموضوع ، ولكن لا يزال هناك الكثير من المفاهيم الخاطئة حوله. سنحاول تصحيح الموقف ومضغ المواد قدر الإمكان ، دون التضحية بالعمق أو الفهم.

الموضوعات / الفصول المشمولة:
- مفهوم عدم التزامن - فوائد عدم التزامن والخرافات حول موضوع "محظور"
- TAP. بناء الجملة وشروط الترجمة - المتطلبات الأساسية لكتابة طريقة الترجمة
- اعمل باستخدام TAP - ميكانيكا وسلوك البرنامج في الكود غير المتزامن (تحرير سلاسل الرسائل ، بدء المهام وانتظار استكمالها)
- وراء الكواليس: آلة الحالة - نظرة عامة على تحويلات المترجم والفئات التي ينشئها
- أصول عدم التزامن. جهاز الطرق غير المتزامنة القياسية - طرق غير متزامنة للعمل مع الملفات والشبكة من الداخل
- فئات TAP وحيلها عبارة عن حيل مفيدة يمكن أن تساعدك في إدارة برنامج وتسريعه باستخدام TAP
مفهوم غير متزامن
عدم التزامن في حد ذاته أبعد ما يكون عن الجديد. غالبًا ما يتضمن التزامن إجراء عملية بأسلوب لا يعني حظر مؤشر ترابط الاتصال ، أي بدء العملية دون انتظار اكتمالها. الحجب ليس شرًا كما هو موصوف. قد يصادف المرء الادعاءات بأن الخيوط المحظورة تهدر وقت وحدة المعالجة المركزية ، وتعمل ببطء أكثر وتسبب المطر. هل يبدو هذا الأخير غير مرجح؟ في الواقع ، فإن 2 نقطة السابقة هي نفسها.
على مستوى جدولة نظام التشغيل ، عندما يكون الخيط في حالة "محظورة" ، فلن يتم تخصيص وقت ثمين للمعالج. مكالمات جدولة ، كقاعدة عامة ، تقع على العمليات التي تسبب حظر المقاطعات جهاز ضبط الوقت و مقاطعات أخرى. وهذا هو ، على سبيل المثال ، عندما تكمل وحدة تحكم القرص عملية القراءة وتبدأ مقاطعة مناسبة ، يبدأ المجدول. سوف يقرر ما إذا كان سيتم بدء تشغيل سلسلة رسائل تم حظرها بواسطة هذه العملية ، أم سيحدد موضوعًا آخر له أولوية أعلى.
العمل البطيء يبدو أكثر سخافة. في الواقع ، في الواقع ، العمل واحد. ستعمل العملية غير المتزامنة فقط على إضافة مقدار أكبر قليلاً من الحمل.
تحدي المطر ليس عمومًا شيئًا من هذه المنطقة.
مشكلة الحجب الرئيسية هي الاستهلاك غير المعقول لموارد الكمبيوتر. حتى لو نسينا وقت إنشاء سلسلة رسائل والعمل مع مجموعة مؤشرات ترابط ، فإن كل مؤشر ترابط محظور يستهلك مساحة إضافية. حسنًا ، هناك سيناريوهات حيث يمكن لمؤشر ترابط واحد فقط أداء عمل معين (على سبيل المثال ، مؤشر ترابط واجهة المستخدم). وفقًا لذلك ، لا أريده أن يكون مشغولًا بمهمة يمكن أن يقوم بها مؤشر ترابط آخر ، والتضحية بأداء العمليات الخاصة به.
يعد عدم التزامن مفهومًا واسعًا للغاية ويمكن تحقيقه بعدة طرق.
يمكن تمييز ما يلي في محفوظات .NET :
- EAP (نمط غير متزامن قائم على الأحداث) - كما يدل الاسم ، فإن الارتفاع يستند إلى الأحداث التي تنطلق عند اكتمال العملية والطريقة المعتادة التي تستدعي هذه العملية
- APM (نموذج البرمجة غير المتزامن) - يعتمد على طريقتين. إرجاع الأسلوب BeginSmth واجهة IAsyncResult. يقبل أسلوب EndSmth IAsyncResult (إذا لم يتم إكمال العملية بحلول الوقت الذي يتم فيه استدعاء EndSmth ، يتم حظر مؤشر الترابط)
- TAP (النمط غير المتزامن القائم على المهام) هو نفس التزامن / الانتظار (بالمعنى الدقيق للكلمة ، ظهرت هذه الكلمات بعد النهج وأنواع المهمة والمهمة <<النتيجة> ظهرت ، لكن المزامنة / الانتظار تحسنت بشكل كبير هذا المفهوم)
كان النهج الأخير ناجحًا لدرجة أن الجميع نسوا بنجاح تلك السابقة. لذلك ، سيكون عنه.
نمط غير متزامن يستند إلى المهام. بناء الجملة وشروط التجميع
طريقة غير متزامنة على غرار TAP القياسية هي سهلة جدا للكتابة.
للقيام بذلك ، تحتاج إلى :
- لتكون قيمة الإرجاع مهمة أو مهمة <T> أو لاغية (غير مستحسن ، تمت مناقشتها لاحقًا). في C # 7 جاءت أنواع تشبه المهام (تمت مناقشتها في الفصل الأخير). في C # 8 ، تتم إضافة IAsyncEnumerable <T> و IAsyncEnumerator <T> إلى هذه القائمة.
- بحيث يتم تمييز الطريقة بالكلمة الرئيسية غير المتزامن وتحتوي على الانتظار بداخلها. هذه الكلمات الرئيسية مقترنة. علاوة على ذلك ، إذا كانت الطريقة تحتوي على انتظار ، فتأكد من وضع علامة على أنها غير متزامنة ، والعكس غير صحيح ، لكنه بلا فائدة
- من أجل الحشمة ، يجب الالتزام بمعاهدة لاحقة Async. بالطبع ، فإن المترجم لن يعتبر هذا خطأ. إذا كنت مطورًا لائقًا للغاية ، فيمكنك إضافة أحمال زائدة باستخدام أداة إلغاء العلامة (التي تمت مناقشتها في الفصل الأخير)
لمثل هذه الأساليب ، يقوم المترجم بعمل جدي. ويصبحون غير معروفين تمامًا من وراء الكواليس ، لكن المزيد عن ذلك لاحقًا.
وقد ذكر أن الطريقة يجب أن تحتوي على الكلمة الرئيسية في انتظار. تشير (الكلمة) إلى الحاجة إلى الانتظار غير المتزامن للمهمة المراد تنفيذها ، وهو كائن المهمة الذي يتم تطبيقه عليه.
يحتوي كائن المهمة أيضًا على شروط معينة بحيث يمكن تطبيق الانتظار عليها:- يجب أن يحتوي النوع المتوقع على أسلوب GetAwaiter () عام (أو داخلي) ، كما يمكن أن يكون وسيلة امتداد. هذه الطريقة تقوم بإرجاع كائن انتظار.
- يجب أن يقوم كائن الانتظار بتطبيق واجهة INotifyCompletion ، والتي تتطلب تطبيق الأسلوب OnCompleted (Action Action) الفارغ. يجب أن يحتوي أيضًا على IsCompleted bool لخاصية مثيل ، أسلوب GetResult () الخالي. يمكن أن يكون إما هيكل أو فئة.
يوضح المثال التالي كيفية جعل int متوقعًا ، وحتى لا يتم تنفيذه مطلقًا.
تمديد كثافة العملياتpublic class Program { public static async Task Main() { await 1; } } public static class WeirdExtensions { public static AnyTypeAwaiter GetAwaiter(this int number) => new AnyTypeAwaiter(); public class AnyTypeAwaiter : INotifyCompletion { public bool IsCompleted => false; public void OnCompleted(Action continuation) { } public void GetResult() { } } }
العمل مع TAP
من الصعب الخوض في الغابة دون فهم كيفية عمل شيء ما. النظر في TAP من حيث سلوك البرنامج.
في المصطلحات: الطريقة غير المتزامنة قيد النظر ، والتي سيتم النظر في الكود الخاص بها ، سأتصل
بالطريقة غير المتزامنة ، والطرق غير المتزامنة المدعومة بداخلها ، والتي
سأدعوها العملية غير المتزامنة .
لنأخذ أبسط مثال ، كعملية غير متزامنة نأخذ Task.Delay ، والتي تؤخر الوقت المحدد دون حظر الدفق.
public static async Task DelayOperationAsync()
تنفيذ الطريقة من حيث السلوك على النحو التالي.
- يتم تنفيذ كل التعليمات البرمجية التي تسبق استدعاء العملية غير المتزامنة. في هذه الحالة ، هذا هو الأسلوب BeforeCall
- استدعاء عملية غير متزامن قيد التقدم. في هذه المرحلة ، لا يتم تحرير الخيط أو حظره. تقوم هذه العملية بإرجاع النتيجة - كائن المهمة المذكور (عادةً المهمة) ، والذي يتم تخزينه في متغير محلي
- يتم تنفيذ الرمز بعد استدعاء العملية غير المتزامنة ، ولكن قبل الانتظار (في انتظار). في المثال - AfterCall
- في انتظار الانتهاء من كائن المهمة (الذي يتم تخزينه في متغير محلي) - في انتظار المهمة.
إذا اكتملت العملية غير المتزامنة في هذه المرحلة ، فسيستمر التنفيذ بشكل متزامن في نفس الخيط.
في حالة عدم اكتمال العملية غير المتزامنة ، يتم حفظ الرمز الذي يجب استدعاؤه عند إكمال العملية غير المتزامنة (ما يسمى استمرار) ، ويعود الدفق إلى تجمع مؤشرات الترابط ويصبح متاحًا للاستخدام. - يتم تنفيذ العمليات بعد الانتظار - AfterAwait - إما على الفور ، في نفس الخيط عند اكتمال العملية في وقت الانتظار ، أو عند الانتهاء من العملية ، يتم أخذ خيط جديد سيستمر (يتم حفظه في الخطوة السابقة)
وراء الكواليس. آلة الدولة
في الواقع ، يتم تحويل الأسلوب الخاص بنا بواسطة برنامج التحويل البرمجي إلى طريقة كعب حيث يتم تهيئة الفئة التي تم إنشاؤها - آلة الحالة -. ثم يبدأ (الجهاز) في العمل ، ويتم إرجاع كائن المهمة المستخدم في الخطوة 2 من الطريقة.
ذات أهمية خاصة هي طريقة
MoveNext لآلة الحالة. هذه الطريقة تفعل ما كانت عليه قبل التحويل بالطريقة غير المتزامنة. إنه يكسر الرمز بين كل انتظار مكالمة. يتم تنفيذ كل جزء في حالة معينة من الجهاز. يتم إرفاق الأسلوب
MoveNext نفسه إلى كائن الانتظار كمتابعة. إن الحفاظ على الدولة يضمن تنفيذ هذا الجزء بالتحديد الذي يتبع التوقع المنطقي.
كما يقولون ، من الأفضل أن ترى مرة واحدة بدلاً من أن تسمع 100 مرة ، لذلك أوصي بشدة أن تتعرف على المثال التالي. أعدت كتابة الكود قليلاً ، وحسّنت التسمية المتغيرة ، وعلقت بسخاء.
شفرة المصدر public static async Task Delays() { Console.WriteLine(1); await Task.Delay(1000); Console.WriteLine(2); await Task.Delay(1000); Console.WriteLine(3); await Task.Delay(1000); Console.WriteLine(4); await Task.Delay(1000); Console.WriteLine(5); await Task.Delay(1000); }
طريقة كعب [AsyncStateMachine(typeof(DelaysStateMachine))] [DebuggerStepThrough] public Task Delays() { DelaysStateMachine stateMachine = new DelaysStateMachine(); stateMachine.taskMethodBuilder = AsyncTaskMethodBuilder.Create(); stateMachine.currentState = -1; AsyncTaskMethodBuilder builder = stateMachine.taskMethodBuilder; taskMethodBuilder.Start(ref stateMachine); return stateMachine.taskMethodBuilder.Task; }
آلة الدولة [CompilerGenerated] private sealed class DelaysStateMachine : IAsyncStateMachine {
أركز على عبارة "في هذه المرحلة لم يتم تنفيذها بشكل متزامن." يمكن أن تتبع العملية غير المتزامنة أيضًا مسار التنفيذ المتزامن. الشرط الرئيسي للطريقة غير المتزامنة الحالية ليتم تنفيذها بشكل متزامن ، وهذا هو ، دون تغيير الخيط ، هو إكمال العملية غير المتزامنة في وقت التحقق
IsCompleted .
يوضح هذا المثال بوضوح هذا السلوك. static async Task Main() { Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
حول سياق التزامن.
يؤدي الأسلوب
AwaitUnsafeOnCompleted المستخدم في الجهاز في النهاية إلى استدعاء الأسلوب
Task.SetContinuationForAwait . في هذه الطريقة ، يتم استرداد سياق المزامنة الحالي
SynchronizationContext.Current . يمكن تفسير سياق التزامن كنوع من الدفق. إذا كانت محددة أيضًا (على سبيل المثال ، سياق مؤشر ترابط واجهة المستخدم) ، يتم إنشاء استمرارية باستخدام فئة
SynchronizationContextAwaitTaskContinuation . يستدعي هذا الفصل لبدء المتابعة طريقة النشر في السياق المحفوظ ، مما يضمن تنفيذ المتابعة في السياق المحدد حيث تم تشغيل الطريقة. يعتمد المنطق المحدد لتنفيذ الاستمرارية على طريقة
النشر في سياق ، بعبارة ملطفة ، غير معروف بالسرعة. إذا لم يكن هناك سياق التزامن (أو تمت الإشارة إلى أنه لا يهمنا في أي سياق سيستمر التنفيذ باستخدام ConfigureAwait (false) ، والذي سيتم مناقشته في الفصل الأخير) ، سيتم تنفيذ المتابعة بواسطة سلسلة الرسائل من التجمع.
أصول عدم التزامن. طرق الجهاز القياسية غير متزامن
نظرنا في طريقة استخدام المزامنة وانتظار المظهر وما يحدث وراء الكواليس. هذه المعلومات ليست غير شائعة. ولكن من المهم أن نفهم طبيعة العمليات غير المتزامنة. لأنه ، كما رأينا في جهاز الحالة ، يتم استدعاء العمليات غير المتزامنة في الكود ، ما لم تتم معالجة نتيجتها بطريقة أكثر دهاء. ومع ذلك ، ماذا يحدث داخل العمليات غير المتزامنة نفسها؟ ربما نفس الشيء ، ولكن هذا لا يمكن أن يحدث بلا حدود.
مهمة مهمة هي فهم طبيعة عدم التزامن. عند محاولة فهم عدم التزامن ، هناك تناوب بين الحالات "أصبح واضحًا الآن" و "الآن غير مفهوم من جديد". وسيكون هذا التناوب حتى يتم فهم مصدر عدم التزامن.
عند العمل مع عدم التزامن ، نعمل على المهام. هذا ليس في كل نفس الدفق. يمكن تنفيذ مهمة واحدة عن طريق العديد من مؤشرات الترابط ، ويمكن تنفيذ مؤشر واحد العديد من المهام.
يبدأ عدم التزامن عادةً بأسلوب يُرجع المهمة (على سبيل المثال) ، ولكن لا يتم وضع علامة باسم غير متزامن ، وبالتالي لا يستخدم الانتظار بداخله. لا تتسامح هذه الطريقة مع أي تغييرات مترجم ؛ يتم تنفيذها كما هي.
لذلك ، دعونا ننظر إلى بعض جذور عدم التزامن.- Task.Run ، Task (..) جديدة. ابدأ () ، Factory.StartNew وما شابه. أسهل طريقة لبدء التنفيذ غير المتزامن. هذه الطرق ببساطة إنشاء كائن مهمة جديدة ، ويمر المفوض كواحدة من المعلمات. يتم نقل المهمة إلى برنامج الجدولة ، مما يعطيها ليتم تنفيذها بواسطة أحد مؤشرات الترابط في التجمع. يتم إرجاع المهمة النهائية التي يمكن توقعها. عادةً ما يتم استخدام هذا الأسلوب لبدء الحوسبة (مرتبط CPU) في مؤشر ترابط منفصل.
- TaskCompletionSource. فئة مساعدة تساعد في التحكم في كائن المهمة. مصممة لأولئك الذين لا يستطيعون تخصيص مفوض للتنفيذ ويستخدم آليات أكثر تطورا للسيطرة على الانتهاء. لديه واجهة برمجة تطبيقات بسيطة للغاية - SetResult ، SetError ، إلخ ، والتي تقوم بتحديث المهمة وفقًا لذلك. هذه المهمة متاحة من خلال خاصية المهمة. ربما في داخلك ستقوم بإنشاء سلاسل رسائل ، ولديك منطق معقد لتفاعلها أو إكمالها حسب الحدث. مزيد من التفاصيل حول هذا الفصل ستكون في القسم الأخير.
في فقرة إضافية ، يمكنك جعل أساليب المكتبات القياسية. وتشمل هذه الملفات قراءة / كتابة ، العمل مع شبكة ، وما شابه. كقاعدة عامة ، تستخدم هذه الطرق الشائعة والمشتركة مكالمات النظام التي تختلف على منصات مختلفة ، وأجهزتهم مسلية للغاية. النظر في العمل مع الملفات والشبكة.
ملفات
ملاحظة مهمة - إذا كنت ترغب في العمل مع الملفات ، يجب عليك تحديد useAsync = true عند إنشاء FileStream.
يتم ترتيب كل شيء في ملفات غير تافهة ومربكة. يتم تعريف فئة FileStream جزئيًا. وإلى جانب ذلك ، هناك 6 وظائف إضافية خاصة بالنظام الأساسي. لذلك ، في Unix ، يستخدم الوصول غير المتزامن إلى ملف تعسفي ، كقاعدة عامة ، عملية متزامنة في سلسلة رسائل منفصلة. في نظام Windows ، هناك دعوات للنظام لعملية غير متزامنة ، والتي يتم استخدامها بالطبع. هذا يؤدي إلى اختلافات في العمل على منصات مختلفة.
المصادر .
يونكسيتمثل السلوك القياسي عند الكتابة أو القراءة في إجراء العملية بشكل متزامن ، إذا كان المخزن المؤقت يسمح بذلك ولم يكن الدفق مشغولًا بعملية أخرى:
1. دفق ليست مشغول مع عملية أخرى
تحتوي فئة Filestream على كائن موروث من SemaphoreSlim مع المعلمات (1 ، 1) - أي مقطع حرج - يمكن تنفيذ جزء التعليمات البرمجية المحمي بواسطة هذا الإشارة بواسطة مؤشر ترابط واحد فقط في كل مرة. يتم استخدام هذا الإشارة على حد سواء للقراءة والكتابة. وهذا يعني أنه من المستحيل إنتاج كل من القراءة والكتابة في وقت واحد. في هذه الحالة ، لا يحدث حظر على الإشارة. يتم استدعاء الأسلوب this._asyncState.WaitAsync () عليه ، والذي يُرجع كائن مهمة (لا يوجد قفل أو انتظار ، فسيكون الأمر إذا تم تطبيق الكلمة الرئيسية "انتظار" على نتيجة الطريقة). إذا لم يتم إكمال كائن المهمة هذا - أي ، يتم التقاط الإشارة ، فسيتم إرفاق المتابعة (Task.ContinueWith) التي يتم فيها تنفيذ العملية بكائن الانتظار الذي تم إرجاعه. إذا كان الكائن مجانيًا ، فأنت بحاجة إلى التحقق مما يلي
2. يسمح العازلة
هنا يعتمد السلوك بالفعل على طبيعة العملية.
للتسجيل - يتم التحقق من أن حجم البيانات الخاصة بالكتابة + الموضع في الملف أقل من حجم المخزن المؤقت ، والذي يكون افتراضيًا 4096 بايت. بمعنى ، يجب أن نكتب 4096 بايت من البداية ، و 2048 بايت مع إزاحة 2048 ، وهكذا. إذا كانت هذه هي الحالة ، فسيتم تنفيذ العملية بشكل متزامن ، وإلا تكون المتابعة متصلة (Task.ContinueWith). تتمة يستخدم استدعاء نظام متزامن منتظم. عند امتلاء المخزن المؤقت ، تتم كتابته على القرص بشكل متزامن.
للقراءة - يتم التحقق مما إذا كانت هناك بيانات كافية في المخزن المؤقت من أجل إرجاع جميع البيانات اللازمة. إذا لم يكن كذلك ، إذن ، مرة أخرى ، استمرار (Task.ContinueWith) مع استدعاء نظام متزامن.
بالمناسبة ، هناك تفاصيل مثيرة للاهتمام. إذا كان جزء واحد من البيانات يشغل المخزن المؤقت بأكمله ، فسيتم كتابته مباشرة إلى الملف ، دون مشاركة المخزن المؤقت. في الوقت نفسه ، هناك موقف حيث سيكون هناك المزيد من البيانات من حجم المخزن المؤقت ، لكنها سوف تمر عبره. يحدث هذا إذا كان هناك بالفعل شيء في المخزن المؤقت. بعد ذلك ، سيتم تقسيم بياناتنا إلى جزأين ، أحدهما سوف يملأ المخزن المؤقت حتى النهاية وسيتم كتابة البيانات إلى الملف ، أما الثاني فسيتم كتابته إلى المخزن المؤقت إذا وصل إليه أو مباشرة إلى الملف إذا لم يفعل. لذلك ، إذا أنشأنا دفقًا وكتبنا 4097 بايت له ، فسوف يظهرون على الفور في الملف ، دون استدعاء Dispose. إذا كتبنا 4095 ، فلن يكون هناك شيء في الملف.
نوافذتحت Windows ، خوارزمية استخدام المخزن المؤقت والكتابة مباشرة متشابهة للغاية. ولكن لوحظ اختلاف كبير مباشرة في النظام غير المتزامن الكتابة وقراءة المكالمات. التحدث دون التوجه إلى أعماق مكالمات النظام ، هناك مثل هذا الهيكل المتداخل. لديها مجال مهم بالنسبة لنا - HANDLE hEvent. هذا هو حدث إعادة الضبط اليدوي الذي يذهب إلى حالة الإنذار عند الانتهاء من العملية. العودة إلى التنفيذ. الكتابة مباشرة ، وكذلك الكتابة إلى المخزن المؤقت ، استخدم مكالمات النظام غير المتزامن ، والتي تستخدم الهيكل أعلاه كمعلمة. عند التسجيل ، يتم إنشاء كائن FileStreamCompletionSource - ورث TaskCompletionSource ، حيث تم تحديد IOCallback. يتم استدعاؤه بواسطة مؤشر ترابط مجاني من التجمع عند اكتمال العملية. في رد الاتصال ، يتم تحليل بنية Overlapped ويتم تحديث كائن المهام وفقًا لذلك. هذا كله سحر.
شبكة
من الصعب وصف كل شيء رأيته يفهم المصدر. تكمن مساري من HttpClient إلى Socket وإلى SocketAsyncContext لـ Unix. المخطط العام هو نفسه كما هو الحال مع الملفات. بالنسبة لنظام Windows ، يتم استخدام بنية Overlapped المذكورة ويتم تنفيذ العملية بشكل غير متزامن. في نظام التشغيل Unix ، تستخدم عمليات الشبكة أيضًا وظائف رد الاتصال.
وشرح قليلا. سوف يلاحظ القارئ اليقظ أنه عند استخدام مكالمات غير متزامنة بين مكالمة واستدعاء ، هناك فراغ معين يعمل بطريقة ما مع البيانات. هنا يجدر التوضيح للتأكد من اكتماله. في مثال الملفات ، يقوم جهاز التحكم بالقرص بإجراء عمليات مباشرة مع القرص بواسطة وحدة التحكم في القرص ، وهو الذي يعطي الإشارات حول نقل الرؤوس إلى القطاع المطلوب ، إلخ. المعالج مجاني في هذا الوقت. يحدث الاتصال مع القرص من خلال منافذ الإدخال / الإخراج. وهي تشير إلى نوع العملية ، وموقع البيانات على القرص ، إلخ. بعد ذلك ، تعمل وحدة التحكم والقرص في هذه العملية وعند الانتهاء من العمل ، يتم إنشاء مقاطعة. وفقًا لذلك ، فإن مكالمة النظام غير المتزامن لا تساهم إلا في المعلومات في منافذ الإدخال / الإخراج ، بينما ينتظر المتزامن أيضًا النتائج ، ويضع الدفق في حالة حظر. لا يدعي هذا المخطط أنه دقيق تمامًا (لا يتعلق بهذه المقالة) ، ولكنه يعطي فهمًا مفاهيميًا للعمل.
الآن طبيعة العملية واضحة. ولكن قد يسأل شخص ما ، ماذا تفعل مع عدم التزامن؟ من المستحيل أن تكتب المتزامن على طريقة إلى الأبد.
بادئ ذي بدء. يمكن تقديم الطلب كخدمة. في هذه الحالة ، تتم كتابة نقطة الدخول - الرئيسية - من البداية. حتى وقت قريب ، لا يمكن أن يكون Main غير متزامن ؛ في الإصدار 7 من اللغة ، تمت إضافة هذه الميزة. لكنه لا يغير أي شيء بشكل جذري ، فقط المترجم يولد الرئيسي المعتاد ، ومن غير المتزامن فقط يتم إنشاء طريقة ثابتة ، والتي تسمى في الرئيسية ويتوقع اكتمالها بشكل متزامن. لذلك ، على الأرجح لديك بعض الأعمال التي طال أمدها. لسبب ما ، في هذه اللحظة ، يبدأ العديد من الأشخاص في التفكير في كيفية إنشاء سلاسل رسائل لهذا العمل: من خلال Task أو ThreadPool أو Thread بشكل عام يدويًا ، لأنه يجب أن يكون هناك اختلاف في شيء ما. الجواب بسيط - بالطبع المهمة. إذا كنت تستخدم أسلوب TAP ، فلا تتداخل مع إنشاء سلسلة رسائل يدوية. هذا أقرب إلى استخدام HttpClient لجميع الطلبات تقريبًا ، ويتم POST بشكل مستقل من خلال Socket.
ثانيا. تطبيقات الويب. يؤدي كل طلب وارد إلى سحب مؤشر ترابط جديد من ThreadPool للمعالجة. المجموعة ، بالطبع ، كبيرة ، ولكن ليست لانهائية. في حالة وجود الكثير من الطلبات ، قد لا يكون هناك عدد كافٍ من مؤشرات الترابط على الإطلاق ، وسيتم وضع جميع الطلبات الجديدة في قائمة الانتظار للمعالجة. هذا الوضع يسمى الجوع. ولكن في حالة استخدام وحدات التحكم غير المتزامنة ، كما تمت مناقشته مسبقًا ، يعود الدفق إلى التجمع ويمكن استخدامه لمعالجة الطلبات الجديدة. وبالتالي ، يتم زيادة إنتاجية الخادم بشكل كبير.
نظرنا إلى العملية غير المتزامنة من البداية إلى النهاية.
ومسلحين بفهم كل هذا غير المتزامن ، الذي يتناقض مع الطبيعة البشرية ، سننظر في بعض الحيل المفيدة عند العمل مع رمز غير متزامن.فصول وحيل مفيدة عند العمل مع TAP
التنوع الثابت لفئة المهمة.
تحتوي فئة المهام على العديد من الأساليب الثابتة المفيدة. فيما يلي أهمها.- Task.WhenAny (..) عبارة عن أداة دمج تأخذ IEnumerable / params من كائنات المهام وتقوم بإرجاع كائن مهمة سيتم إكماله عند اكتمال المهمة الأولى التي يتم إكمالها. وهذا يعني أنه يسمح لك بالانتظار لأحد المهام العديدة الجارية
- Task.WhenAll (..) - أداة الجمع ، وتقبل IEnumerable / params لكائنات المهمة وإرجاع كائن المهمة ، الذي سيتم إكماله عند الانتهاء من جميع المهام المنقولة
- Task.FromResult<T>(T value) — , .
- Task.Delay(..) —
- Task.Yield() — . , . , ,
ConfigureAwait
بطبيعة الحال ، الميزة الأكثر تقدما "المتقدمة". تنتمي هذه الطريقة إلى فئة المهام وتتيح لك تحديد ما إذا كنا بحاجة إلى المتابعة في نفس السياق الذي تم استدعاء العملية غير المتزامنة فيه. بشكل افتراضي ، دون استخدام هذه الطريقة ، يتم تذكر السياق ومتابعته باستخدام طريقة النشر المذكورة. ومع ذلك ، كما قلنا ، بوست هو متعة مكلفة للغاية. لذلك ، إذا كان الأداء في المركز الأول ، ورأينا أن المتابعة لن تقوم ، على سبيل المثال ، بتحديث واجهة المستخدم ، يمكنك تحديد .ConfigureAwait (false) على كائن الانتظار . هذا يعني أنه لا يهمنا أين سيتم تنفيذ الاستمرارية.الآن عن المشكلة. كما يقولون ، مخيف ليس الجهل ، ولكن المعرفة الخاطئة.لقد حدث أنني لاحظت بطريقة ما رمز تطبيق ويب ، حيث تم تزيين كل مكالمة غير متزامنة مع هذا المعجل. هذا ليس له أي تأثير بخلاف الاشمئزاز البصري. لا يحتوي تطبيق الويب ASP.NET Core القياسي على أي سياقات فريدة (ما لم تكتبها بنفسك ، بالطبع). وبالتالي ، لا يتم استدعاء أسلوب المشاركة هناك على أي حال.TaskCompletionSource <T>
فئة تجعل من السهل إدارة كائن المهمة. يتمتع الفصل بفرص وافرة ، ولكنه مفيد للغاية عندما نريد أن نختتم مهمة بإجراء ما ، وتنتهي نهايته في حدث ما. بشكل عام ، تم إنشاء الفصل لتكييف الأساليب غير المتزامنة القديمة مع TAP ، ولكن كما رأينا ، يتم استخدامه ليس فقط لهذا الغرض. مثال صغير للعمل مع هذه الفئة:مثال public static Task<string> GetSomeDataAsync() { TaskCompletionSource<string> tcs = new TaskCompletionSource<string>(); FileSystemWatcher watcher = new FileSystemWatcher { Path = Directory.GetCurrentDirectory(), NotifyFilter = NotifyFilters.LastAccess, EnableRaisingEvents = true }; watcher.Changed += (o, e) => tcs.SetResult(e.FullPath); return tcs.Task; }
تقوم هذه الفئة بإنشاء برنامج التفاف غير متزامن للحصول على اسم الملف الذي تم الوصول إليه في المجلد الحالي.CancellationTokenSource
يسمح لك بإلغاء عملية غير متزامنة. يشبه المخطط التفصيلي العام استخدام TaskCompletionSource. أولاً ، يتم إنشاء var cts = new CancellationTokenSource () ، والذي ، بالمناسبة ، هو IDisposable ، ثم cts.Token يتم تمريره إلى عمليات غير متزامنة . علاوة على ذلك ، باتباع بعض المنطق ، في ظل ظروف معينة ، يتم استدعاء طريقة cts.Cancel () . يمكنه أيضًا الاشتراك في حدث أو أي شيء آخر.استخدام CancellationToken هو ممارسة جيدة. عند كتابة طريقة غير متزامنة تقوم ببعضها في الخلفية ، على سبيل المثال لا الحصر ، يمكنك ببساطة إدراج سطر واحد في نص الحلقة: cancellationToken.ThrowIfCancellationRequested () ، والذي سيؤدي إلى استثناءOperationCanceledException . يتم التعامل مع هذا الاستثناء على أنه إلغاء للعملية ولا يتم حفظه كاستثناء داخل كائن المهمة. أيضاً ، ستصبح الخاصية IsCanceled على كائن المهام صحيحاً.LongRunning
غالبًا ما تكون هناك مواقف ، خاصة عند كتابة الخدمات ، عند إنشاء العديد من المهام التي ستعمل طوال فترة الخدمة أو لفترة طويلة جدًا. كما نتذكر ، فإن استخدام تجمع مؤشرات الترابط يعد أمرًا مبررًا لإنشاء مؤشر ترابط. ومع ذلك ، إذا نادراً ما يتم إنشاء دفق (حتى مرة واحدة في الساعة) ، فسيتم تسوية هذه التكاليف ويمكنك إنشاء دفق منفصل بأمان. للقيام بذلك ، عند إنشاء مهمة ، يمكنك تحديد خيار خاص:Task.Factory.StartNew (إجراء ، TaskCreationOptions.LongRunning )على أي حال ، أنصحك بالاطلاع على جميع التحميلات الزائدة Task.Factory.StartNew ، هناك العديد من الطرق لتكوين المهمة بمرونة لتلبية الاحتياجات المحددة.استثناءات
نظرًا للطبيعة غير الحتمية لتنفيذ التعليمات البرمجية غير المتزامنة ، فإن مسألة الاستثناءات مهمة للغاية. سيكون من العار إذا لم تتمكن من الاستثناء وتم طرحه في الخيط الأيسر ، مما أدى إلى مقتل العملية. تم إنشاء فئة ExceptionDispatchInfo لالتقاط استثناء في مؤشر ترابط واحد ورميها فيه . لالتقاط الاستثناء ، يتم استخدام الأسلوب الثابت ExceptionDispatchInfo.Capture (ex) ، والذي يقوم بإرجاع ExceptionDispatchInfo.يمكن تمرير ارتباط إلى هذا الكائن إلى أي مؤشر ترابط ، والذي يستدعي طريقة Throw () لرميها بعيدًا. لا يحدث رمي نفسه في مكان استدعاء عملية غير متزامنة ، ولكن في مكان استخدام عامل الانتظار. وكما تعلم ، لا يمكن أن تنتظر حتى يتم تطبيقها. وبالتالي ، إذا كان السياق موجودًا ، فسيتم تمريره إليه بواسطة طريقة النشر. خلاف ذلك ، سيكون متحمس في الدفق من التجمع. وهذا هو ما يقرب من 100 ٪ مرحبا إلى انهيار التطبيق. وهنا نأتي إلى حقيقة أننا يجب أن نستخدم المهمة أو المهمة <T> ، لكن ليس باطلاً.وأكثر شيء واحد. يحتوي المجدول على حدث TaskScheduler.UnobservedTaskException الذي يتم تشغيله عند طرح UnobservedTaskException. يتم طرح هذا الاستثناء أثناء تجميع البيانات المهملة عندما يحاول GC تجميع كائن مهمة له استثناء غير معالَج.IAsyncEnumerable
قبل الإصدار C # 8 و .NET Core 3.0 ، لم يكن من الممكن استخدام مكرر العائد بطريقة غير متزامنة ، مما أدى إلى تعقيد الحياة وجعلها تُرجع المهمة <IEnumerable <T>> من هذه الطريقة ، أي لم يكن هناك طريقة للتكرار من خلال المجموعة حتى تم استلامها بالكامل. الآن هناك مثل هذه الفرصة. تعلم المزيد عن ذلك هنا . لهذا ، يجب أن يكون نوع الإرجاع IAsyncEnumerable <T> (أو IAsyncEnumerator <T> ). لاجتياز مثل هذه المجموعة ، يجب عليك استخدام حلقة foreach مع الكلمة الرئيسية التي تنتظر الانتظار. أيضا ، يمكن استدعاء أساليب WithCancellation و ConfigureAwait على نتيجة العملية ، مما يشير إلى CancelationToken المستخدمة والحاجة إلى المتابعة في نفس السياق.كما هو متوقع ، يتم كل شيء كسول قدر الإمكان.أدناه مثال وخلاصة يعطيها.مثال public class Program { public static async Task Main() { Stopwatch sw = new Stopwatch(); sw.Start(); IAsyncEnumerable<int> enumerable = AsyncYielding(); Console.WriteLine($"Time after calling: {sw.ElapsedMilliseconds}"); await foreach (var element in enumerable.WithCancellation(..).ConfigureAwait(false)) { Console.WriteLine($"element: {element}"); Console.WriteLine($"Time: {sw.ElapsedMilliseconds}"); } } static async IAsyncEnumerable<int> AsyncYielding() { foreach (var uselessElement in Enumerable.Range(1, 3)) { Task task = Task.Delay(TimeSpan.FromSeconds(uselessElement)); Console.WriteLine($"Task run: {uselessElement}"); await task; yield return uselessElement; } } }
الخلاصة:الوقت بعد الاتصال: 0
تشغيل المهام:
عنصر واحد : 1
الوقت: 1033
تشغيل المهمة: 2
عنصر: 2
الوقت: 3034
تشغيل المهمة: 3
عنصر: 3
الوقت: 6035ThreadPool
يستخدم هذا الفصل بنشاط عند البرمجة باستخدام TAP. لذلك ، سأقدم الحد الأدنى من تفاصيل تنفيذه. في الداخل ، يحتوي ThreadPool على مجموعة من قوائم الانتظار: واحدة لكل خيط + عام واحد. عند إضافة وظيفة جديدة إلى التجمع ، يتم مراعاة سلسلة الرسائل التي بدأت الإضافة. إذا كان موضوعًا من التجمع ، فسيتم وضع العمل في قائمة انتظار خاصة به لهذا الموضوع ، إذا كان موضوعًا آخر - في سلسلة عمومية. عند تحديد مؤشر ترابط للعمل ، تظهر قائمة انتظاره المحلية أولاً. إذا كان فارغًا ، فسيأخذ مؤشر الترابط وظائف من المستوى العالمي. إذا كان فارغًا ، يبدأ في السرقة من الآخرين. أيضًا ، يجب ألا تعتمد أبدًا على ترتيب العمل ، لأنه في الواقع ، لا يوجد أمر. يعتمد العدد الافتراضي لمؤشرات الترابط في التجمع على العديد من العوامل ، بما في ذلك حجم مساحة العنوان. إذا كان هناك المزيد من الطلبات للتنفيذ ،من عدد مؤشرات الترابط المتاحة ، الطلبات في قائمة الانتظار.مؤشرات الترابط في تجمع مؤشرات الترابط هي مؤشرات ترابط الخلفية (الخاصية isBackground = true). لا يدعم هذا النوع من سلاسل العمليات عمر العملية في حالة اكتمال كافة سلاسل العمليات الأمامية.يراقب مؤشر ترابط النظام حالة مؤشر الانتظار. عندما تنتهي عملية الانتظار ، يتم تنفيذ رد الاتصال المنقول بواسطة سلسلة الرسائل من التجمع (تذكر الملفات الموجودة في نظام Windows).تشبه مهمة نوع
المذكورة سابقًا ، يمكن استخدام هذا النوع (بنية أو فئة) كقيمة الإرجاع من الأسلوب غير المتزامن. يجب أن يرتبط نوع الباني بهذا النوع باستخدام السمة [AsyncMethodBuilder (..)] . يجب أن يكون لهذا النوع الخصائص المذكورة أعلاه حتى يتمكن من تطبيق الكلمة الرئيسية التي تنتظرها. يمكن أن تكون معلمة للطرق التي لا تُرجع قيمة والمعلمات لتلك التي ترجع.المنشئ نفسه عبارة عن فئة أو هيكل يظهر إطاره في المثال أدناه. يحتوي الأسلوب SetResult على معلمة من النوع T لنوع يشبه المهمة تم تحديده بواسطة T. بالنسبة للأنواع غير المعلمة ، لا تحتوي الطريقة على معلمات.مطلوب منشئ واجهة class MyTaskMethodBuilder<T> { public static MyTaskMethodBuilder<T> Create(); public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine; public void SetStateMachine(IAsyncStateMachine stateMachine); public void SetException(Exception exception); public void SetResult(T result); public void AwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine; public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine; public MyTask<T> Task { get; } }
سيتم شرح مبدأ العمل من وجهة نظر كتابة نوع مهامك أدناه. تم وصف معظم هذا بالفعل عند تحليل الكود الذي تم إنشاؤه بواسطة المترجم.يستخدم المترجم كل هذه الأنواع لإنشاء جهاز حالة. يعرف المحول البرمجي منشئي الإنشاءات الذين يستخدمون الأنواع التي يعرفها ، وهنا نحدد ما سيتم استخدامه أثناء إنشاء التعليمات البرمجية. إذا كانت آلة الحالة عبارة عن هيكل ، فسيتم تعبئتها عند استدعاء SetStateMachine ، يمكن للباني تخزين النسخة المعبأة مؤقتًا إذا لزم الأمر. يجب أن يقوم المنشئ باستدعاء stateMachine.MoveNext في طريقة البدء أو بعد استدعائه لبدء التنفيذ ودفع جهاز الحالة. بعد الاتصال ابدأ، سيتم إرجاع الخاصية Task من الأسلوب. أوصي بالعودة إلى طريقة كعب الروتين وعرض هذه الخطوات.في حالة اكتمال جهاز الحالة بنجاح ، يتم استدعاء الأسلوب SetResult ، وإلا SetException . في حالة وصول جهاز الحالة إلى الانتظار ، يتم تنفيذ الأسلوب GetAwaiter () من النوع الذي يشبه المهمة. إذا كان كائن الانتظار يطبق واجهة ICriticalNotifyCompletion و IsCompleted = false ، فإن جهاز الحالة يستخدم builder.AwaitUnsafeOnCompleted (المرجع في انتظار ، ref stateMachine) . طريقة AwaitUnsafeOnCompleted يجب استدعاء awaiter.OnCompleted (العمل) ، في العمل يجب استدعاء stateMachine.MoveNextعند اكتمال كائن الانتظار. وبالمثل، فإن واجهة INotifyCompletion وطريقة builder.AwaitOnCompleted .كيفية استخدام هذا الأمر متروك لك. لكنني أنصحك بالتفكير في 514 مرة قبل تطبيق هذا في الإنتاج ، وليس للتدليل. فيما يلي مثال للاستخدام. لقد رسمت فقط وكيلًا لباني قياسي يعرض على وحدة التحكم الطريقة التي تم استدعاؤها وفي أي وقت. بالمناسبة ، لا يريد Main (غير المتزامن) دعم نوع مخصص من التوقع (أعتقد أن أكثر من مشروع إنتاج تالف بشكل ميؤوس منه بسبب هذه ملكة جمال بواسطة Microsoft). إذا كنت ترغب في ذلك ، يمكنك تعديل مسجل الوكيل باستخدام مسجل عادي وتسجيل مزيد من المعلومات.تسجيل وكيل المهمة public class Program { public static void Main() { Console.WriteLine("Start"); JustMethod().Task.Wait();
الخلاصة: طريقةالبدء
: إنشاء ؛ 2019-10-09T17: 55: 13.7152733 + 03: 00
الطريقة: ابدأ ؛ 2019-10-09T17: 55: 13.7262226 + 03: 00
الطريقة: AwaitUnsafeOnCompleted؛ 2019-10-09T17: 55: 13.7275206 + 03: 00
الخاصية: Task؛ 2019-10-09T17: 55: 13.7292005 + 03: 00
الطريقة: SetResult؛ 2019-10-09T17: 55: 14.7297967 + 03: 00
توقفهذا كل شيء ، شكراً لكم جميعًا.