هذه المقالة قديمة جدًا ، لكنها لم تفقد أهميتها. عندما يتعلق الأمر بالتزامن / الانتظار ، يظهر رابط إليها عادة. لم أجد ترجمة إلى اللغة الروسية ، قررت مساعدة شخص لا يجيد اللغة الإنجليزية.
كانت البرمجة غير المتزامنة منذ فترة طويلة مملكة المطورين الأكثر خبرة مع شغف الماسوشية - أولئك الذين لديهم وقت فراغ كاف ، والميل والقدرة النفسية على التفكير في عمليات الاسترجاعات من عمليات الاسترجاعات في تدفق غير خطي للتنفيذ. مع ظهور Microsoft .NET Framework 4.5 ، جلبت C # و Visual Basic لنا جميعًا بشكل غير متزامن ، لذلك أصبح بمقدور مجرد بشر أن يكتبوا طرقًا غير متزامنة بنفس السهولة تقريبًا مثل تلك المتزامنة. لم تعد هناك حاجة لردود الفعل. لا رمز تنظيم أكثر صراحة من سياق التزامن واحد إلى آخر. لا مزيد من المخاوف بشأن كيفية تنفيذ نتائج التنفيذ أو الاستثناءات. ليست هناك حاجة إلى الحيل التي تشوه وسائل لغات البرمجة لراحة تطوير التعليمات البرمجية غير المتزامنة. باختصار ، لا يوجد المزيد من المتاعب والصداع.
بالطبع ، على الرغم من أنه من السهل الآن البدء في كتابة طرق غير متزامنة (انظر مقالات إريك ليبرت ومادس تورجيرسن في مجلة MSDN هذه [OCTOBER 2011] ) ، فإن الفهم مطلوب للقيام بذلك بشكل صحيح. ماذا يحدث تحت الغطاء. في كل مرة ترفع لغة أو مكتبة مستوى التجريد الذي يمكن للمطور استخدامه ، يكون هذا مصحوبًا حتماً بتكاليف خفية تقلل الإنتاجية. في كثير من الحالات ، تكون هذه التكاليف ضئيلة ، لذلك يمكن إهمالها في معظم الحالات من قبل معظم المبرمجين. ومع ذلك ، يجب على المطورين المتقدمين فهم التكاليف الحالية بشكل كامل من أجل اتخاذ التدابير اللازمة وحل المشكلات المحتملة إذا كانوا يعبرون عن أنفسهم. هذا مطلوب عند استخدام أدوات البرمجة غير المتزامنة في C # و Visual Basic.
في هذه المقالة ، سوف أصف مدخلات ومخرجات الطرق غير المتزامنة ، وشرح كيفية تنفيذ الطرق غير المتزامنة ، ومناقشة بعض التكاليف الأصغر. لاحظ أن هذه ليست توصية لتشويه التعليمات البرمجية القابلة للقراءة إلى شيء يصعب الحفاظ عليه ، باسم microoptimization والأداء. هذه فقط المعرفة التي ستساعد في تشخيص المشكلات التي قد تواجهها ، ومجموعة من الأدوات للتغلب على هذه المشاكل. بالإضافة إلى ذلك ، تستند هذه المقالة إلى معاينة الإصدار 4.5 من .NET Framework ، وربما تتغير تفاصيل التنفيذ المحددة في الإصدار النهائي.
احصل على نموذج تفكير مريح
منذ عقود ، يستخدم المبرمجون لغات البرمجة عالية المستوى C # و Visual Basic و F # و C ++ لتطوير تطبيقات مثمرة. سمحت هذه التجربة للمبرمجين بتقييم تكاليف العمليات المختلفة واكتساب المعرفة حول أفضل تقنيات التطوير. على سبيل المثال ، في معظم الحالات ، يكون استدعاء طريقة متزامنة أمرًا اقتصاديًا نسبيًا ، خاصةً إذا كان بإمكان برنامج التحويل البرمجي تضمين محتويات الطريقة التي تم استدعاءها مباشرةً في نقطة الاتصال. لذلك ، اعتاد المطورين على تقسيم الشفرة إلى طرق صغيرة يسهل صيانتها ، دون الحاجة إلى القلق بشأن العواقب السلبية لزيادة عدد المكالمات. تم تصميم نموذج التفكير لهؤلاء المبرمجين للتعامل مع استدعاءات الطريقة.
مع ظهور أساليب غير متزامنة ، مطلوب نموذج جديد للتفكير. بإمكان C # و Visual Basic مع برامج التحويل البرمجي الخاصة بهما إنشاء وهم بأن الطريقة غير المتزامنة تعمل كنظير متزامن لها ، على الرغم من أن كل شيء خاطئ تمامًا في الداخل. ينشئ المحول البرمجي مقدارًا كبيرًا من التعليمات البرمجية للمبرمج ، مشابهًا جدًا للقالب القياسي الذي كتبه المطورون لدعم عدم التزامن خلال الوقت الذي كان من الضروري القيام به يدويًا. علاوة على ذلك ، تحتوي التعليمة البرمجية التي أنشأها المترجم على استدعاءات لوظائف مكتبة .NET Framework ، مما يقلل من حجم العمل الذي يحتاج مبرمج إلى القيام به. من أجل الحصول على النموذج الصحيح للتفكير واستخدامه لاتخاذ قرارات مستنيرة ، من المهم أن نفهم ما يولده المترجم لك.
المزيد من الطرق ، عدد أقل من المكالمات
عند العمل برمز متزامن ، فإن تشغيل الأساليب ذات المحتوى الفارغ لا قيمة له عملياً. بالنسبة للطرق غير المتزامنة ، ليست هذه هي الحالة. ضع في اعتبارك هذه الطريقة غير المتزامنة ، التي تتكون من تعليمة واحدة (والتي ، بسبب عدم وجود عبارات انتظار ، سيتم تنفيذها بشكل متزامن):
public static async Task SimpleBodyAsync() { Console.WriteLine("Hello, Async World!"); }
سيكشف أداة فك شفرة اللغة الوسيطة (IL) عن المحتويات الحقيقية لهذه الوظيفة بعد التحويل البرمجي ، مما ينتج عنه شيء مشابه للشكل 1. ما الذي تم تحويله إلى خط واحد بسيط تم تحويله إلى طريقتين ، أحدهما ينتمي إلى الفئة المساعدة من آلة الحالة. الأول هو أسلوب كعب رقيق له توقيع مشابه لذلك المكتوب من قبل المبرمج (هذه الطريقة لها نفس الاسم ونفس النطاق ، وتستغرق المعلمات نفسها وتُرجع نفس النوع) ، ولكنها لا تحتوي على رمز مكتوب بواسطة المبرمج. أنه يحتوي فقط على معيار المرجل للإعداد الأولي. يقوم رمز الإعداد الأولي بتهيئة جهاز الحالة المطلوب لتمثيل الطريقة غير المتزامنة ، ويبدأ تشغيله باستخدام استدعاء إلى أداة الأداة المساعدة MoveNext. يحتوي نوع كائن جهاز الحالة على متغير مع حالة تنفيذ الطريقة غير المتزامنة ، مما يسمح لك بحفظه عند التبديل بين نقاط الانتظار غير المتزامنة. كما أنه يحتوي على رمز مكتوب بواسطة مبرمج ، تم تعديله لضمان نقل نتائج التنفيذ والاستثناءات إلى كائن المهام المرتجع ؛ الاحتفاظ بالموضع الحالي في الطريقة بحيث يمكن تنفيذ التنفيذ من هذا الموقف بعد الاستئناف ، إلخ.
الشكل 1 الشكل 1. غير متزامن قالب الأسلوب
[DebuggerStepThrough] public static Task SimpleBodyAsync() { <SimpleBodyAsync>d__0 d__ = new <SimpleBodyAsync>d__0(); d__.<>t__builder = AsyncTaskMethodBuilder.Create(); d__.MoveNext(); return d__.<>t__builder.Task; } [CompilerGenerated] [StructLayout(LayoutKind.Sequential)] private struct <SimpleBodyAsync>d__0 : <>t__IStateMachine { private int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public Action <>t__MoveNextDelegate; public void MoveNext() { try { if (this.<>1__state == -1) return; Console.WriteLine("Hello, Async World!"); } catch (Exception e) { this.<>1__state = -1; this.<>t__builder.SetException(e); return; } this.<>1__state = -1; this.<>t__builder.SetResult(); } ... }
عندما تتساءل عن تكلفة المكالمات للطرق غير المتزامنة ، تذكر هذا النمط. هناك حاجة إلى كتلة try / catch في طريقة MoveNext لمنع محاولة محتملة لتضمين JIT بواسطة هذه الطريقة بواسطة برنامج التحويل البرمجي ، لذلك على الأقل نحصل على تكلفة استدعاء الطريقة ، بينما عند استخدام الطريقة المتزامنة ، فإن هذه المكالمة على الأرجح لن (بشرط أن محتوى أضيق الحدود). سوف نتلقى عدة مكالمات لإجراءات Framework (على سبيل المثال ، SetResult). وكذلك العديد من عمليات الكتابة في حقول كائن آلة الدولة. بالطبع ، نحن بحاجة إلى مقارنة كل هذه التكاليف بتكاليف Console.WriteLine ، والتي من المحتمل أن تسود (تشمل تكاليف القفل ، I / O ، إلخ.) انتبه إلى التحسينات التي تجعلها البيئة لك. على سبيل المثال ، يتم تطبيق كائن من آلة الحالة كهيكل (هيكل). سيتم تثبيت هذه البنية في كومة مُدارة فقط إذا كانت الطريقة تحتاج إلى إيقاف التنفيذ ، في انتظار انتهاء العملية ، ولن يحدث هذا أبدًا في هذه الطريقة البسيطة. لذلك لن يتطلب نمط هذه الطريقة غير المتزامنة تخصيص ذاكرة من الكومة. سيحاول برنامج التحويل البرمجي ووقت التشغيل تقليل عدد عمليات تخصيص الذاكرة.
عندما لا تستخدم المتزامن
يحاول .NET Framework إنشاء تطبيقات فعالة للطرق غير المتزامنة باستخدام أساليب التحسين المختلفة. ومع ذلك ، فإن المطورين ، استنادًا إلى خبرتهم ، غالبًا ما يطبقون طرق التحسين الخاصة بهم ، والتي يمكن أن تكون محفوفة بالمخاطر وغير عملية للتشغيل الآلي من قبل المترجم ووقت التشغيل ، حيث يحاولون استخدام الأساليب الشاملة. إذا لم تنسَ ذلك ، فسيكون رفض استخدام طرق المزامنة مفيدًا في عدد من الحالات المحددة ، وينطبق هذا بشكل خاص على الأساليب في المكتبات التي يمكن استخدامها مع إعدادات أدق. يحدث هذا عادةً عندما يكون معروفًا بالتأكيد أنه يمكن تنفيذ الطريقة بشكل متزامن ، لأن البيانات التي تعتمد عليها جاهزة بالفعل.
عند إنشاء طرق غير متزامنة ، قضى مطورو برنامج .NET Framework الكثير من الوقت في تحسين عدد عمليات إدارة الذاكرة. يعد ذلك ضروريًا لأن إدارة الذاكرة تتحمل أعلى تكلفة في أداء بنية أساسية غير متزامنة. عملية تخصيص الذاكرة لكائن عادة ما تكون غير مكلفة نسبيا. يشبه تخصيص الذاكرة للأشياء ملء العربة بمنتجات في السوبر ماركت - لا تنفق أي شيء عندما تضعها في العربة. يحدث الإنفاق عندما تدفع عند الخروج ، وتخرج محفظتك وتعطي أموالاً مناسبة. وإذا كان تخصيص الذاكرة أمرًا سهلاً ، فإن تجميع البيانات المهملة اللاحقة يمكن أن يؤثر بشدة على أداء التطبيق. عند بدء تجميع البيانات المهملة ، يتم إجراء مسح وتحديد الكائنات الموجودة حاليًا في الذاكرة ولكن ليس لديها روابط. كلما تم وضع المزيد من الكائنات ، كلما طال الوقت لوضع علامات عليها. بالإضافة إلى ذلك ، كلما زاد عدد الكائنات كبيرة الحجم الموضوعة ، كلما زادت الحاجة إلى تجميع البيانات المهملة. هذا الجانب من العمل مع الذاكرة له تأثير عالمي على النظام: كلما زاد إنتاج القمامة بطرق غير متزامنة ، كلما كان التطبيق أبطأ ، حتى لو لم تثبت microtests تكاليف كبيرة.
بالنسبة للطرق غير المتزامنة التي تعلق تنفيذها (في انتظار البيانات غير الجاهزة بعد) ، يجب على البيئة إنشاء كائن من النوع Task ، سيتم إرجاعه من الطريقة ، لأن هذا الكائن يعمل كمرجع فريد للمكالمة. ومع ذلك ، يمكن إجراء مكالمات غير متزامنة في كثير من الأحيان دون تعليق. ثم يمكن لوقت التشغيل إرجاع كائن المهام المكتمل مسبقًا من ذاكرة التخزين المؤقت ، والذي يتم استخدامه مرارًا وتكرارًا دون الحاجة إلى إنشاء كائنات مهمة جديدة. صحيح ، لا يُسمح بذلك إلا في ظل ظروف معينة ، على سبيل المثال ، عندما تُرجع الطريقة غير المتزامنة كائنًا غير عام (غير عام) المهمة ، أو المهمة ، أو عندما تكون المهمة العامة محددة بواسطة نوع مرجع TResult ، ويتم إرجاع فارغة من هذه الطريقة. على الرغم من أن قائمة هذه الشروط تتوسع بمرور الوقت ، إلا أنها لا تزال أفضل إذا كنت تعرف كيفية تنفيذ العملية.
النظر في تنفيذ هذا النوع باعتباره MemoryStream. يرث MemoryStream من Stream ، ويعيد تعريف الأساليب الجديدة المطبقة في .NET 4.5: ReadAsync و WriteAsync و FlushAsync ، من أجل توفير تعليمة برمجية خاصة بالذاكرة. نظرًا لإجراء عملية القراءة من مخزن مؤقت موجود في الذاكرة ، أي أنها في الواقع نسخة من منطقة الذاكرة ، سيكون أفضل أداء إذا تم تنفيذ ReadAsync في الوضع المتزامن. قد يبدو تنفيذ هذا بطريقة غير متزامنة كما يلي:
public override async Task<int> ReadAsync(byte [] buffer, int offset, int count, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); return this.Read(buffer, offset, count); }
بسيطة بما فيه الكفاية. ونظرًا لأن Read هي مكالمة متزامنة ، ولا تحتوي الطريقة على بيانات انتظار للتحكم في التوقعات ، فسيتم بالفعل تنفيذ جميع المكالمات إلى ReadAsync بشكل متزامن. الآن لنلقِ نظرة على الحالة القياسية لاستخدام سلاسل الرسائل ، على سبيل المثال ، عملية النسخ:
byte [] buffer = new byte[0x1000]; int numRead; while((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) > 0) { await source.WriteAsync(buffer, 0, numRead); }
يرجى ملاحظة أنه في مثال ReadAsync المحدد ، يتم استدعاء دفق المصدر دائمًا بنفس معلمة طول المخزن المؤقت ، مما يعني أنه من المحتمل جدًا تكرار قيمة الإرجاع (عدد وحدات البايت المقروءة). باستثناء في بعض الحالات النادرة ، من غير المحتمل أن يستخدم تطبيق ReadAsync كائن المهام المخزنة مؤقتًا كقيمة إرجاع ، ولكن يمكنك القيام بذلك.
النظر في خيار تنفيذ آخر لهذه الطريقة ، كما هو موضح في الشكل 2. باستخدام مزايا الجوانب الكامنة في البرامج النصية القياسية لهذه الطريقة ، يمكننا تحسين التنفيذ من خلال استبعاد عمليات تخصيص الذاكرة ، والتي من غير المرجح أن تكون متوقعة من وقت التشغيل. يمكننا القضاء على فقدان الذاكرة تمامًا عن طريق إرجاع نفس كائن المهمة الذي تم استخدامه في استدعاء ReadAsync السابق إذا تمت قراءة نفس عدد وحدات البايت. وبالنسبة لمثل هذه العملية منخفضة المستوى ، والتي من المحتمل أن تكون سريعة جدًا وسيتم استدعاؤها مرارًا وتكرارًا ، سيكون لهذا التحسين تأثير كبير ، خاصة في عدد مجموعات البيانات المهملة.
الشكل 2 الشكل 2. الأمثل لخلق المهمة
private Task<int> m_lastTask; public override Task<int> ReadAsync(byte [] buffer, int offset, int count, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { var tcs = new TaskCompletionSource<int>(); tcs.SetCanceled(); return tcs.Task; } try { int numRead = this.Read(buffer, offset, count); return m_lastTask != null && numRead == m_lastTask.Result ? m_lastTask : (m_lastTask = Task.FromResult(numRead)); } catch(Exception e) { var tcs = new TaskCompletionSource<int>(); tcs.SetException(e); return tcs.Task; } }
يمكن استخدام طريقة أمثل مماثلة عن طريق التخلص من الإنشاء غير الضروري لكائنات المهام إذا كان التخزين المؤقت ضروريًا. ضع في اعتبارك طريقة مصممة لاسترداد محتويات صفحة ويب وتخزينها مؤقتًا للرجوع إليها مستقبلاً. كأسلوب غير متزامن ، يمكن كتابة هذا كالتالي (باستخدام مكتبة System.Net.Http.dll الجديدة لـ .NET 4.5):
private static ConcurrentDictionary<string,string> s_urlToContents; public static async Task<string> GetContentsAsync(string url) { string contents; if (!s_urlToContents.TryGetValue(url, out contents)) { var response = await new HttpClient().GetAsync(url); contents = response.EnsureSuccessStatusCode().Content.ReadAsString(); s_urlToContents.TryAdd(url, contents); } return contents; }
هذا هو تنفيذ الجبين. بالنسبة إلى استدعاءات GetContentsAsync التي لا تعثر على بيانات في ذاكرة التخزين المؤقت ، يمكن إهمال مقدار الحمل لإنشاء كائن مهمة جديد مقارنة بتكلفة تلقي البيانات عبر الشبكة. ومع ذلك ، في حالة الحصول على البيانات من ذاكرة التخزين المؤقت ، تصبح هذه التكاليف كبيرة إذا قمت ببساطة بلف البيانات المحلية وإتاحتها.
للتخلص من هذه التكاليف (إذا لزم الأمر لتحقيق أداء عالٍ) ، يمكنك إعادة كتابة الطريقة كما هو موضح في الشكل 3. الآن لدينا طريقتان: طريقة عامة متزامنة وطريقة خاصة غير متزامنة ، يقوم المندوبون العموميون بإصدارها. تقوم مجموعة Dictionary الآن بتخزين كائنات المهام التي تم إنشاؤها مؤقتًا ، وليس محتوياتها ، لذلك يمكن إجراء المحاولات المستقبلية لاسترداد محتويات الصفحة التي تم الحصول عليها بنجاح من خلال الوصول ببساطة إلى المجموعة لإرجاع كائن المهام الحالي. من الداخل ، يمكنك الاستفادة من استخدام أساليب ContinueWith لكائن المهام ، مما يسمح لنا بحفظ الكائن الذي تم تنفيذه في المجموعة - في حالة نجاح تحميل الصفحة. بالطبع ، هذا الرمز أكثر تعقيدًا ويتطلب الكثير من التطوير والدعم ، كالمعتاد عند تحسين الأداء: لا ترغب في قضاء بعض الوقت في كتابته حتى يظهر اختبار الأداء أن هذه التعقيدات تؤدي إلى تحسينها ، وهو أمر مثير للإعجاب وواضح. ما هي التحسينات التي تعتمد في الواقع على طريقة التطبيق. يمكنك تجربة مجموعة اختبار تحاكي حالات الاستخدام الشائعة وتقييم النتائج لتحديد ما إذا كانت اللعبة تستحق الشمعة أم لا.
الشكل 3 الشكل 3. المهام التخزين المؤقت يدويا
private static ConcurrentDictionary<string,Task<string>> s_urlToContents; public static Task<string> GetContentsAsync(string url) { Task<string> contents; if (!s_urlToContents.TryGetValue(url, out contents)) { contents = GetContentsInternalAsync(url); contents.ContinueWith(delegate { s_urlToContents.TryAdd(url, contents); }, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuatOptions.ExecuteSynchronously, TaskScheduler.Default); } return contents; } private static async Task<string> GetContentsInternalAsync(string url) { var response = await new HttpClient().GetAsync(url); return response.EnsureSuccessStatusCode().Content.ReadAsString(); }
تتمثل طريقة التحسين الأخرى المرتبطة بكائنات المهام في تحديد ما إذا كان يجب إرجاع مثل هذا الكائن من الطريقة غير المتزامنة على الإطلاق. يدعم كل من C # و Visual Basic أساليب غير متزامنة تُرجع قيمة فارغة (خالية) ، ولا تنشئ كائنات مهمة على الإطلاق. يجب أن تُرجع الطرق غير المتزامنة في المكتبات المهام والمهام دائمًا ، لأنه عند تصميم مكتبة لا يمكنك معرفة أنه لن يتم استخدامها في انتظار الانتهاء. ومع ذلك ، عند تطوير التطبيقات ، يمكن أن تجد الطرق التي تُرجع الفراغ مكانها. السبب الرئيسي لوجود مثل هذه الطرق هو توفير البيئات القائمة على الأحداث القائمة ، مثل ASP.NET و Windows Presentation Foundation (WPF). باستخدام المزامنة والانتظار ، تسهل هذه الطرق تنفيذ معالجات الأزرار وأحداث تحميل الصفحة ، إلخ. إذا كنت تنوي استخدام طريقة غير متزامنة مع الفراغ ، فاحرص على معالجة الاستثناءات: ستظهر الاستثناءات منه في أي SynchronizationContext الذي كان نشطًا في وقت تم استدعاء الطريقة.
لا تنس السياق
هناك العديد من السياقات المختلفة في .NET Framework: LogicalCallContext و SynchronizationContext و HostExecutionContext و SecurityContext و ExecutionContext وغيرها (قد يشير حجمها الضخم إلى أن منشئي Framework قد تم تحفيزهم مالياً لإنشاء سياقات جديدة ، لكنني أعرف بالتأكيد أن هذا ليس كذلك). تؤثر بعض هذه السياقات بشدة على الأساليب غير المتزامنة ، ليس فقط من حيث الأداء الوظيفي ، ولكن أيضًا في الأداء.
SynchronizationContext SynchronizationContext يلعب دورًا هامًا للطرق غير المتزامنة. "سياق التزامن" هو مجرد تجريد لضمان تنظيم الاحتجاج المفوض مع تفاصيل مكتبة أو بيئة معينة. على سبيل المثال ، يحتوي WPF على DispatcherSynchronizationContext لتمثيل دفق واجهة مستخدم (UI) لـ Dispatcher: يؤدي إرسال مفوض إلى سياق المزامنة هذا إلى وضع هذا المفوض في قائمة الانتظار للتنفيذ من قبل Dispatcher في دفقه. يوفر ASP.NET AspNetSynchronizationContext يُستخدم للتأكد من أن العمليات غير المتزامنة المتضمنة في معالجة طلب ASP.NET مضمونة ليتم تنفيذها بالتسلسل وتكون مرتبطة بحالة HttpContext الصحيحة. حسنا ، الخ بشكل عام ، هناك حوالي 10 تخصصات من SynchronizationContext في .NET Framework ، بعضها مفتوح ، وبعضه داخلي.
عند انتظار مهام أو كائنات من الأنواع الأخرى التي يمكن لـ .NET Framework تنفيذ ذلك ، تلتقط الكائنات التي تنتظرها (على سبيل المثال ، TaskAwaiter) SynchronizationContext الحالي في الوقت الذي يبدأ فيه الانتظار (قيد الانتظار). عند الانتهاء من الانتظار ، في حالة التقاط SynchronizationContext ، يتم إرسال استمرار الأسلوب غير المتزامن إلى سياق المزامنة هذا. لهذا السبب ، لا يحتاج المبرمجون الذين يكتبون أساليب غير متزامنة والتي يتم استدعاؤها من دفق واجهة المستخدم إلى تنظيم المكالمات يدويًا مرة أخرى إلى دفق واجهة المستخدم من أجل تحديث عناصر تحكم واجهة المستخدم: ينفذ إطار العمل هذا التنظيم تلقائيًا.
لسوء الحظ ، فإن هذا التنظيم يأتي بسعر. لمطوري التطبيقات الذين يستخدمون الانتظار لتنفيذ تدفق التحكم ، فإن الحشد التلقائي هو الحل الصحيح. غالبًا ما يكون للمكتبات قصة مختلفة تمامًا. لمطوري التطبيقات ، يعد هذا التنظيم ضروريًا بشكل أساسي للرمز للتحكم في السياق الذي يتم تنفيذه فيه ، على سبيل المثال ، للوصول إلى عناصر تحكم واجهة المستخدم أو للوصول إلى HttpContext المطابق لطلب ASP.NET المطلوب. ومع ذلك ، لا يُطلب من المكتبات عمومًا تلبية هذا المطلب. نتيجةً لذلك ، غالباً ما يجلب التنظيم التلقائي تكاليف إضافية غير ضرورية تمامًا. دعنا نلقي نظرة أخرى على الكود الذي يقوم بنسخ البيانات من دفق إلى آخر:
byte [] buffer = new byte[0x1000]; int numRead; while((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) > 0) { await source.WriteAsync(buffer, 0, numRead); }
إذا تم استدعاء هذه النسخة من دفق واجهة المستخدم ، فستجبر كل عملية قراءة وكتابة التنفيذ على العودة إلى دفق واجهة المستخدم. في حالة وجود ميغا بايت من البيانات في المصدر والتدفقات التي تقرأ وتكتب بشكل غير متزامن (أي معظم تطبيقاتها) ، فإن هذا يعني أن حوالي 500 التبديل من دفق الخلفية إلى دفق واجهة المستخدم. لمعالجة هذا السلوك في أنواع المهام والمهام ، يتم إنشاء أسلوب ConfigureAwait. يقبل هذا الأسلوب المعلمة ContinueOnCapturedContext لنوع منطقي يتحكم في التنظيم. إذا كان هذا صحيحًا (الافتراضي) ، فانتظر تلقائيًا إرجاع التحكم إلى SynchronizationContext الملتقطة. إذا تم استخدام false ، فسيتم تجاهل سياق التزامن ، وستستمر البيئة في تنفيذ العملية غير المتزامنة في سلسلة العمليات حيث تمت مقاطعة ذلك. سيؤدي تطبيق هذا المنطق إلى إصدار نسخة أكثر كفاءة من رمز النسخ بين سلاسل الرسائل:
byte [] buffer = new byte[0x1000]; int numRead; while((numRead = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0) { await source.WriteAsync(buffer, 0, numRead).ConfigureAwait(false); }
لمطوري المكتبات ، مثل هذا التسارع في حد ذاته يكفي للتفكير دائمًا في استخدام ConfigureAwait ، باستثناء الحالات النادرة التي تعرف فيها المكتبة ما يكفي عن وقت التشغيل وستحتاج إلى تنفيذ الطريقة مع الوصول إلى السياق الصحيح.
بالإضافة إلى الأداء ، هناك سبب آخر تحتاج إلى استخدام ConfigureAwait عند تطوير المكتبات. تخيل أن أسلوب CopyStreamToStreamAsync المطبق مع إصدار الكود بدون ConfigureAwait يسمى من دفق UI في WPF ، على سبيل المثال ، مثل هذا:
private void button1_Click(object sender, EventArgs args) { Stream src = …, dst = …; Task t = CopyStreamToStreamAsync(src, dst); t.Wait(); // deadlock! }
في هذه الحالة ، كان على المبرمج أن يكتب button1_Click كطريقة غير متزامنة يتوقع من خلالها أن ينتظر مشغل الانتظار تنفيذ المهمة ، ولا يستخدم طريقة الانتظار المتزامن لهذا الكائن. يجب استخدام طريقة الانتظار في العديد من الحالات الأخرى ، ولكن سيكون من الخطأ دائمًا استخدامها في الانتظار في دفق واجهة المستخدم ، كما هو موضح هنا. لن ترجع طريقة الانتظار حتى تكتمل المهمة. في حالة CopyStreamToStreamAsync ، يحاول دفقه غير المتزامن إرجاع التنفيذ مع إرسال البيانات إلى SynchronizationContext ، ولا يمكن إكماله حتى تكتمل عمليات النقل هذه (لأنها ضرورية لمواصلة تشغيلها). لكن هذه الإرساليات ، بدورها ، لا يمكن تنفيذها ، لأن مؤشر ترابط واجهة المستخدم الذي يجب معالجتها محظور بواسطة مكالمة الانتظار. هذا تبعية دورية تؤدي إلى طريق مسدود. إذا تم تطبيق CopyStreamToStreamAsync باستخدام ConfigureAwait (false) ، فلن يكون هناك تبعية أو حظر.
ExecutionContext ExecutionContext هو جزء مهم من .NET Framework ، ولكن لا يزال معظم المبرمجين غير مدركين لوجوده. ExecutionContext – , SecurityContext LogicalCallContext, , . , ThreadPool.QueueUserWorkItem, Task.Run, Delegate.BeginInvoke, Stream.BeginRead, WebClient.DownloadStringAsync Framework, ExecutionContext ExecutionContext.Run ( ). , , ThreadPool.QueueUserWorkItem, Windows (identity), WaitCallback. , Task.Run LogicalCallContext, LogicalCallContext Action. ExecutionContext .
Framework , ExecutionContext, , . Windows LogicalCallContext . (WindowsIdentity.Impersonate CallContext.LogicalSetData) .
. C# Visual Basic , . await. , , - . C# Visual Basic («») , await (boxed) , .
. , . , , , .
C# Visual Basic , . ,
public static async Task FooAsync() { var dto = DateTimeOffset.Now; var dt = dto.DateTime; await Task.Yield(); Console.WriteLine(dt); }
dto await, . , , - dto:
Figure 4
[StructLayout(LayoutKind.Sequential), CompilerGenerated] private struct <FooAsync>d__0 : <>t__IStateMachine { private int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public Action <>t__MoveNextDelegate; public DateTimeOffset <dto>5__1; public DateTime <dt>5__2; private object <>t__stack; private object <>t__awaiter; public void MoveNext(); [DebuggerHidden] public void <>t__SetMoveNextDelegate(Action param0); }
, . , , , , . , :
public static async Task FooAsync() { var dt = DateTimeOffset.Now.DateTime; await Task.Yield(); Console.WriteLine(dt); }
, .NET (GC) , , , : 0, , , (.NET GC 0, 1 2). , GC . , , , , , , . 0, , , . , , , .
( , ). JIT , , , , . , , . , , , , . , , . , C# Visual Basic , , .
C# Visual Basic , awaits: . await , Task , , . , , :
public static async Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { return Sum(await a, await b, await c); } private static int Sum(int a, int b, int c) { return a + b + c; }
C# “await b” Sum. await, Sum, - async , «» await. , await . , , CLR, , , . , <>t__stack. , , Tuple<int, int> <>__stack. , , , . , SumAsync :
public static async Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { int ra = await a; int rb = await b; int rc = await c; return Sum(ra, rb, rc); }
, ra, rb rc, . , : . , , , . , , , , .
, , . Sum , await , . , await , . await , Task.WhenAll:
public static async Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { int [] results = await Task.WhenAll(a, b, c); return Sum(results[0], results[1], results[2]); }
Task.WhenAll Task<TResult[]>, , , , . . , WhenAll, Task Task. , , , , , WhenAll , . WhenAll, , , params, . , , . Figure 5
Figure 5
public static Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { return (a.Status == TaskStatus.RanToCompletion && b.Status == TaskStatus.RanToCompletion && c.Status == TaskStatus.RanToCompletion) ? Task.FromResult(Sum(a.Result, b.Result, c.Result)) : SumAsyncInternal(a, b, c); } private static async Task<int> SumAsyncInternal(Task<int> a, Task<int> b, Task<int> c) { await Task.WhenAll((Task)a, b, c).ConfigureAwait(false); return Sum(a.Result, b.Result, c.Result); }
, . , . , . , , : , , / , . .NET Framework , . , .NET Framework, . , , Framework, , , .