تم تقديم وظيفة Async / Await في C # 5 لتحسين استجابة واجهة المستخدم والوصول إلى الموارد على الويب. بمعنى آخر ، الأساليب غير المتزامنة تساعد المطورين على إجراء عمليات غير متزامنة لا تحظر مؤشرات الترابط وإرجاع نتيجة مفردة واحدة. بعد محاولات عديدة من Microsoft لتبسيط العمليات غير المتزامنة ، اكتسب قالب async / انتظار سمعة طيبة بين المطورين بفضل طريقة بسيطة.
الأساليب غير المتزامنة الحالية محدودة بشكل كبير حيث يجب أن تُرجع قيمة واحدة فقط. دعونا نلقي نظرة على طريقة async Task<int> DoAnythingAsync()
شائعة في بناء الجملة هذا. نتيجة عمله هو معنى واحد. بسبب هذا القيد ، لا يمكنك استخدام هذه الوظيفة مع الكلمة الأساسية ذات yield
وواجهة IEnumerable<int>
غير المتزامنة (لإرجاع نتيجة تعداد غير متزامن).

إذا قمت بدمج وظيفة async/await
yield
، فيمكنك استخدام نموذج برمجة قوي يعرف باسم سحب البيانات غير المتزامنة ، أو تعداد التعداد القائم على السحب أو تسلسل غير متزامن غير متزامن ، كما يطلق عليه في F #.
تزيل القدرة الجديدة على استخدام مؤشرات الترابط غير المتزامنة في C # 8 القيد المرتبط بإرجاع نتيجة واحدة وتتيح للطريقة غير المتزامنة إرجاع قيم متعددة. ستمنح هذه التغييرات القالب غير المتزامن مرونة أكبر ، وسيكون المستخدم قادرًا على استرداد البيانات من مكان ما (على سبيل المثال ، من قاعدة البيانات) باستخدام تسلسلات غير متزامنة مؤجلة أو لتلقي البيانات من تسلسلات غير متزامنة في الأجزاء كما هو متاح.
مثال:
foreach await (var streamChunck in asyncStreams) { Console.WriteLine($“Received data count = {streamChunck.Count}”); }
هناك طريقة أخرى لحل المشكلات المتعلقة بالبرمجة غير المتزامنة وهي استخدام الامتدادات التفاعلية (Rx). تتزايد أهمية Rx
بين المطورين ويتم استخدام هذه الطريقة في العديد من لغات البرمجة ، مثل Java (RxJava) و JavaScript (RxJS).
يعتمد Rx على نموذج دفع (مبدأ Tell Don't Ask) ، المعروف أيضًا باسم البرمجة التفاعلية. أي بخلاف IEnumerable ، عندما يطلب المستهلك العنصر التالي ، في نموذج Rx ، يشير مزود البيانات إلى المستهلك بأن عنصرًا جديدًا يظهر في التسلسل. يتم دفع البيانات إلى قائمة الانتظار في وضع غير متزامن ويستخدمها المستهلك في وقت الاستلام.
في هذه المقالة ، سأقارن نموذجًا يعتمد على دفع البيانات (مثل Rx) مع نموذج يستند إلى سحب البيانات (مثل IEnumerable) ، وأعرض أيضًا السيناريوهات الأكثر ملاءمة لأي طراز. يتم فحص المفهوم الكامل والفوائد مع مجموعة متنوعة من الأمثلة ورمز التجريبي. في النهاية ، سأعرض التطبيق وأظهره مع مثال الكود.
مقارنة نموذج يعتمد على دفع البيانات مع نموذج يستند إلى سحب البيانات (سحب)

التين. -1- مقارنة نموذج يعتمد على سحب البيانات مع نموذج يعتمد على دفع البيانات
تستند هذه الأمثلة إلى العلاقة بين موفر البيانات والمستهلك ، كما هو موضح في الشكل. -1. يسهل فهم النموذج القائم على السحب. في ذلك ، يطلب المستهلك ويتلقى البيانات من المورد. نهج بديل هو دفع نموذج. هنا ، ينشر الموفر البيانات في قائمة الانتظار ويجب على العميل الاشتراك فيها من أجل الحصول عليها.
يعتبر نموذج سحب البيانات مناسبًا للحالات التي يولد فيها الموفر بيانات أسرع من استخدامها من قبل المستهلك. وبالتالي ، يتلقى المستهلك فقط البيانات اللازمة ، والتي تتجنب مشاكل التدفق. إذا كان المستهلك يستخدم البيانات بشكل أسرع من المورد الذي ينتجها ، فإن النموذج القائم على دفع البيانات مناسب. في هذه الحالة ، يمكن للمورد إرسال المزيد من البيانات إلى المستهلك حتى لا يكون هناك أي تأخير غير ضروري.
تستخدم Rx و Akka Streams (نموذج برمجة قائم على التدفق) طريقة الضغط الخلفي للتحكم في التدفق. لحل مشاكل المورد والمتلقي الموصوف أعلاه ، تستخدم الطريقة كلاً من ضغط البيانات وسحبها.
في المثال أدناه ، يسحب المستهلك البطيء البيانات من مزود أسرع. بعد أن يعالج المستهلك العنصر الحالي ، سيطلب من المورد التالي وما إلى ذلك حتى نهاية التسلسل.
لفهم الحاجة الكاملة إلى مؤشرات ترابط غير متزامن ، خذ بعين الاعتبار التعليمة البرمجية التالية.
// (count) static int SumFromOneToCount(int count) { ConsoleExt.WriteLine("SumFromOneToCount called!"); var sum = 0; for (var i = 0; i <= count; i++) { sum = sum + i; } return sum; } // : const int count = 5; ConsoleExt.WriteLine($"Starting the application with count: {count}!"); ConsoleExt.WriteLine("Classic sum starting."); ConsoleExt.WriteLine($"Classic sum result: {SumFromOneToCount(count)}"); ConsoleExt.WriteLine("Classic sum completed."); ConsoleExt.WriteLine("################################################"); ConsoleExt.WriteLine(Environment.NewLine);
الاستنتاج:

يمكننا أن نجعل الطريقة مؤجلة باستخدام بيان العائد ، كما هو مبين أدناه.
static IEnumerable<int> SumFromOneToCountYield(int count) { ConsoleExt.WriteLine("SumFromOneToCountYield called!"); var sum = 0; for (var i = 0; i <= count; i++) { sum = sum + i; yield return sum; } }
طريقة الدعوة
const int count = 5; ConsoleExt.WriteLine("Sum with yield starting."); foreach (var i in SumFromOneToCountYield(count)) { ConsoleExt.WriteLine($"Yield sum: {i}"); } ConsoleExt.WriteLine("Sum with yield completed."); ConsoleExt.WriteLine("################################################"); ConsoleExt.WriteLine(Environment.NewLine);
الاستنتاج:

كما هو موضح في نافذة الإخراج أعلاه ، يتم إرجاع النتيجة في أجزاء ، وليس لقيمة واحدة. تُعرف النتائج الموجزة الموضحة أعلاه بالقائمة المؤجلة. ومع ذلك ، لا تزال المشكلة لم تحل: طرق التجميع تحظر الكود. إذا نظرت إلى سلاسل العمليات ، يمكنك أن ترى أن كل شيء يعمل في سلسلة الرسائل الرئيسية.
دعنا نطبّق الكلمة السحرية غير المتزامنة على طريقة SumFromOneToCount الأولى (بدون العائد).
static async Task<int> SumFromOneToCountAsync(int count) { ConsoleExt.WriteLine("SumFromOneToCountAsync called!"); var result = await Task.Run(() => { var sum = 0; for (var i = 0; i <= count; i++) { sum = sum + i; } return sum; }); return result; }
طريقة الدعوة
const int count = 5; ConsoleExt.WriteLine("async example starting."); // . , . , . var result = await SumFromOneToCountAsync(count); ConsoleExt.WriteLine("async Result: " + result); ConsoleExt.WriteLine("async completed."); ConsoleExt.WriteLine("################################################"); ConsoleExt.WriteLine(Environment.NewLine);
الاستنتاج:

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

التين. -2- خطأ أثناء استخدام العائد وكلمة أساسية غير متزامن في نفس الوقت.
عندما نحاول إضافة المزامنة إلى SumFromOneToCountYield ، يحدث خطأ كما هو موضح أعلاه.
لنجربها بطريقة مختلفة. يمكننا إزالة الكلمة الرئيسية ذات العائد وتطبيق IEnumerable في المهمة ، كما هو موضح أدناه:
static async Task<IEnumerable<int>> SumFromOneToCountTaskIEnumerable(int count) { ConsoleExt.WriteLine("SumFromOneToCountAsyncIEnumerable called!"); var collection = new Collection<int>(); var result = await Task.Run(() => { var sum = 0; for (var i = 0; i <= count; i++) { sum = sum + i; collection.Add(sum); } return collection; }); return result; }
طريقة الدعوة
const int count = 5; ConsoleExt.WriteLine("SumFromOneToCountAsyncIEnumerable started!"); var scs = await SumFromOneToCountTaskIEnumerable(count); ConsoleExt.WriteLine("SumFromOneToCountAsyncIEnumerable done!"); foreach (var sc in scs) { // , . . ConsoleExt.WriteLine($"AsyncIEnumerable Result: {sc}"); } ConsoleExt.WriteLine("################################################"); ConsoleExt.WriteLine(Environment.NewLine);
الاستنتاج:

كما ترى من المثال ، يتم حساب كل شيء في وضع غير متزامن ، ولكن لا تزال المشكلة قائمة. يتم إرجاع النتائج (يتم جمع كل النتائج في مجموعة) ككتلة واحدة. وهذا ليس ما نحتاجه. إذا كنت تتذكر ، فهدفنا هو الجمع بين وضع الحساب غير المتزامن مع إمكانية التأخير.
للقيام بذلك ، تحتاج إلى استخدام مكتبة خارجية ، على سبيل المثال ، Ix (جزء من Rx) ، أو سلاسل الرسائل غير المتزامنة ، المقدمة في C #.
دعنا نعود إلى كودنا. لإظهار السلوك غير المتزامن ، استخدمت مكتبة خارجية .
static async Task ConsumeAsyncSumSeqeunc(IAsyncEnumerable<int> sequence) { ConsoleExt.WriteLineAsync("ConsumeAsyncSumSeqeunc Called"); await sequence.ForEachAsync(value => { ConsoleExt.WriteLineAsync($"Consuming the value: {value}"); // Task.Delay(TimeSpan.FromSeconds(1)).Wait(); }); } static IEnumerable<int> ProduceAsyncSumSeqeunc(int count) { ConsoleExt.WriteLineAsync("ProduceAsyncSumSeqeunc Called"); var sum = 0; for (var i = 0; i <= count; i++) { sum = sum + i; // Task.Delay(TimeSpan.FromSeconds(0,5)).Wait(); yield return sum; } }
طريقة الدعوة
const int count = 5; ConsoleExt.WriteLine("Starting Async Streams Demo!"); // . . IAsyncEnumerable<int> pullBasedAsyncSequence = ProduceAsyncSumSeqeunc(count).ToAsyncEnumerable(); ConsoleExt.WriteLineAsync("X#X#X#X#X#X#X#X#X#X# Doing some other work X#X#X#X#X#X#X#X#X#X#"); // ; . var consumingTask = Task.Run(() => ConsumeAsyncSumSeqeunc(pullBasedAsyncSequence)); // . , . consumingTask.Wait(); ConsoleExt.WriteLineAsync("Async Streams Demo Done!");
الاستنتاج:

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

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

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

التين. -5 - سحب البيانات كتسلسل غير متزامن (تدفقات غير متزامنة). لا يتم حظر العميل.
المواضيع غير متزامن
مثل IEnumerable<T>
و IEnumerator<T>
هناك IAsyncEnumerable<T>
، IAsyncEnumerable<T>
و IAsyncEnumerator<T>
، وهي معرّفة كما هو موضح أدناه:
public interface IAsyncEnumerable<out T> { IAsyncEnumerator<T> GetAsyncEnumerator(); } public interface IAsyncEnumerator<out T> : IAsyncDisposable { Task<bool> MoveNextAsync(); T Current { get; } } // public interface IAsyncDisposable { Task DiskposeAsync(); }
في InfoQ ، حصل جوناثان ألن على هذا الموضوع بشكل صحيح. هنا لن أخوض في التفاصيل ، لذلك أوصي بقراءة مقالته .
يتم التركيز على القيمة المرجعة Task<bool> MoveNextAsync()
(تم التغيير من bool إلى Task<bool>
، bool IEnumerator.MoveNext()
). بفضله ، ستحدث جميع العمليات الحسابية بالإضافة إلى تكرارها بشكل غير متزامن. يقرر المستهلك موعد الحصول على القيمة التالية. على الرغم من أنه نموذج غير متزامن ، إلا أنه لا يزال يستخدم سحب البيانات. للتنظيف غير المتزامن للموارد ، يمكنك استخدام واجهة IAsyncDisposable
. يمكن العثور على مزيد من المعلومات حول مؤشرات الترابط غير المتزامن هنا .
بناء الجملة
يجب أن يبدو بناء الجملة النهائي كما يلي:
foreach await (var dataChunk in asyncStreams) { // yield . }
من المثال أعلاه ، من الواضح أنه بدلاً من حساب قيمة واحدة ، يمكننا نظريًا حساب مجموعة متتالية من القيم ، أثناء انتظار عمليات أخرى غير متزامنة.
مثال مايكروسوفت المعاد تصميمه
أعيد كتابة رمز العرض التوضيحي لـ Microsoft. يمكن تنزيله بالكامل من مستودع جيثب الخاص بي .
يعتمد المثال على فكرة إنشاء دفق كبير في الذاكرة (صفيف 20000 بايت) واستخراج العناصر منه بشكل متزامن في وضع غير متزامن. أثناء كل تكرار ، يتم سحب 8 كيلو بايت من الصفيف.


في الخطوة (1) ، يتم إنشاء مجموعة بيانات كبيرة ، مليئة بقيم وهمية. ثم ، أثناء الخطوة (2) ، يتم تعريف متغير يسمى المجموع الاختباري. هذا المتغير الذي يحتوي على المجموع الاختباري يهدف إلى التحقق من صحة مجموع الحسابات. يتم إنشاء صفيف ومجموع اختباري في الذاكرة ويتم إرجاعهما كسلسلة من العناصر في الخطوة (3).
تتضمن الخطوة (4) تطبيق AsEnumarble
امتداد AsEnumarble
(اسم أكثر ملاءمة AsAsyncEnumarble) ، مما يساعد على محاكاة دفق غير متزامن من 8 كيلوبايت (BufferSize = 8000 من العناصر (6))
ليس من الضروري عادة أن ترث من IAsyncEnumerable ، ولكن في المثال الموضح أعلاه ، يتم تنفيذ هذه العملية لتبسيط رمز العرض التوضيحي ، كما هو موضح في الخطوة (5).
تتضمن الخطوة (7) استخدام الكلمة الأساسية foreach
، التي تستخرج 8 كيلو بايت من البيانات من دفق غير متزامن في الذاكرة. تحدث عملية السحب بالتتابع: عندما يكون المستهلك (جزء من الكود الذي يحتوي على foreach
) جاهزًا لاستلام الجزء التالي من البيانات ، فإنه يسحبهم من الموفر (الصفيف الموجود في الدفق في الذاكرة). أخيرًا ، عند اكتمال الدورة ، سيقوم البرنامج بالتحقق من قيمة "c" في المجموع الاختباري وإذا كانت متطابقة ، فسوف يعرض رسالة "تطابق المجموع الاختباري!" ، وفقًا للخطوة (8).
نافذة عرض مايكروسوفت التجريبي:

استنتاج
لقد بحثنا في مؤشرات الترابط غير المتزامنة ، والتي تعد كبيرة بالنسبة لسحب البيانات بشكل غير متزامن وكتابة التعليمات البرمجية التي تنشئ قيمًا متعددة في الوضع غير المتزامن.
باستخدام هذا النموذج ، يمكنك الاستعلام عن عنصر البيانات التالي في تسلسل والحصول على استجابة. إنه يختلف عن نموذج دفع البيانات IObservable<T>
، باستخدام القيم التي يتم إنشاؤها بغض النظر عن حالة المستهلك. تسمح لك التدفقات غير المتزامنة بتمثيل مصادر البيانات غير المتزامنة التي يتحكم فيها المستهلك تمامًا عندما يحدد هو نفسه الرغبة في قبول الجزء التالي من البيانات. تتضمن الأمثلة استخدام تطبيقات الويب أو قراءة السجلات في قاعدة بيانات.
لقد أوضحت كيفية إنشاء تعداد في الوضع غير المتزامن واستخدامه باستخدام مكتبة خارجية ذات تسلسل غير متزامن. لقد أوضحت أيضًا الفوائد التي تقدمها هذه الميزة عند تنزيل المحتوى من الإنترنت. أخيرًا ، نظرنا في بناء الجملة الجديد للخيوط غير المتزامنة ، وكذلك مثالًا كاملًا على استخدامه بناءً على رمز العرض التوضيحي لـ Microsoft Build ( 7 مايو ، 2018 // سياتل ، واشنطن )
