ValueTask <النتيجة> - لماذا ولماذا وكيف؟

مقدمة الترجمة


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


مقدمة


تم تقديم مساحة الاسم System.Threading.Tasks وفئة Task لأول مرة في .NET Framework 4. ومنذ ذلك الحين ، دخل هذا النوع وفئة Task<TResult> المشتقة منه Task<TResult> بحزم في ممارسة البرمجة في .NET وأصبحا الجوانب الأساسية للنموذج غير المتزامن. نفذت في C # 5 ، مع async/await . في هذه المقالة ، سأتحدث عن الأنواع الجديدة من ValueTask/ValueTask<TResult> التي تم تقديمها لتحسين أداء التعليمات البرمجية غير المتزامنة ، في الحالات التي تلعب فيها الذاكرة العامة دورًا رئيسيًا.



مهمة


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


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

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


 SomeOperationAsync().ContinueWith(task => { try { TResult result = task.Result; UseResult(result); } catch (Exception e) { HandleException(e); } }); 

باستخدام .NET Frmaework 4.5 و C # 5 ، تم تبسيط عملية الحصول على نتيجة لعملية غير متزامنة من خلال إدخال الكلمات الرئيسية غير async/await والآلية التي تقف وراءها. هذه الآلية ، الكود الذي تم إنشاؤه ، قادرة على تحسين جميع الحالات المذكورة أعلاه ، ومعالجة الإكمال بشكل صحيح على الرغم من المسار الذي تم الوصول إليه.


 TResult result = await SomeOperationAsync(); UseResult(result); 

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


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


 TResult result = await SomeOperationAsync(); UseResult(result); 

هذا مشابه جدًا لكيفية كتابة التعليمات البرمجية المتزامنة (مثل TResult result = SomeOperation(); ). تتم ترجمة هذا الخيار بشكل طبيعي إلى async/await .


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


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


 public async Task WriteAsync(byte value) { if (_bufferedCount == _buffer.Length) { await FlushAsync(); } _buffer[_bufferedCount++] = value; } 

وفي معظم الأحيان ، سيكون هناك مساحة كافية في المخزن المؤقت ، ستنتهي العملية بشكل متزامن. إذا كان الأمر كذلك ، فليس هناك ما يميز المهمة التي يتم إرجاعها ، ولا توجد قيمة إرجاع ، وقد تم إكمال العملية بالفعل. بمعنى آخر ، نحن نتعامل مع Task ، أي ما يعادل عملية void المتزامن. في مثل هذه الحالات ، يقوم وقت التشغيل ببساطة بتخزين كائن Task مؤقتًا ، ويستخدمه في كل مرة كنتيجة لأي async Task غير متزامنة - وهي طريقة تنتهي بشكل متزامن ( Task.ComletedTask ). مثال آخر ، دعنا نقول أن تكتب:


 public async Task<bool> MoveNextAsync() { if (_bufferedCount == 0) { await FillBuffer(); } return _bufferedCount > 0; } 

افترض ، بنفس الطريقة ، أنه في معظم الحالات ، هناك بعض البيانات في المخزن المؤقت. يتحقق الأسلوب _bufferedCount ، ويرى أن المتغير أكبر من الصفر ، وإرجاع true . فقط في حالة التحقق من عدم تخزين البيانات مؤقتًا ، يلزم إجراء عملية غير متزامنة. على true ذلك ، لا يوجد سوى نتيجتين منطقيتين محتملتين ( true false ) ، false فقط للعودة ممكنة من خلال Task<bool> . استنادًا إلى الإكمال المتزامن أو غير المتزامن ، ولكن قبل الخروج من هذه الطريقة ، يقوم وقت التشغيل بتخزين مثيلين من Task<bool> (أحدهما true والآخر false ) ، وإرجاع الحالة المرغوبة ، وتجنب تخصيصات إضافية. الخيار الوحيد عندما يكون لديك لإنشاء كائن Task<bool> جديد هو حالة التنفيذ غير المتزامن ، والذي ينتهي بعد "العودة". في هذه الحالة ، يجب أن تقوم الطريقة بإنشاء كائن Task<bool> ، لأن في وقت الخروج من هذه الطريقة ، لم تكن نتيجة إكمال العملية معروفة بعد. الكائن المرتجع يجب أن يكون فريدًا ، لأن سيتم تخزين نتيجة العملية غير المتزامنة في النهاية.


هناك أمثلة أخرى من التخزين المؤقت مماثلة من وقت التشغيل. لكن هذه الاستراتيجية لا تنطبق في كل مكان. على سبيل المثال ، الطريقة:


 public async Task<int> ReadNextByteAsync() { if (_bufferedCount == 0) { await FillBuffer(); } if (_bufferedCount == 0) { return -1; } _bufferedCount--; return _buffer[_position++]; } 

أيضا في كثير من الأحيان ينتهي بشكل متزامن. ولكن ، على عكس المثال السابق ، تُرجع هذه الطريقة نتيجة عدد صحيح لها حوالي أربعة مليارات قيمة ممكنة. لتخزين Task<int> مؤقتًا Task<int> ، في هذه الحالة ، ستكون هناك حاجة إلى مئات غيغابايت من الذاكرة. تدعم البيئة هنا أيضًا ذاكرة تخزين مؤقت صغيرة Task<int> ، لعدة قيم صغيرة. لذلك ، على سبيل المثال ، إذا اكتملت العملية بشكل متزامن (البيانات موجودة في المخزن المؤقت) ، نتيجة 4 ، سيتم استخدام ذاكرة التخزين المؤقت. ولكن إذا كانت النتيجة ، وإن كانت متزامنة ، فإن الإكمال هو 42 ، سيتم إنشاء كائن Task<int> ، على غرار استدعاء Task.FromResult(42) .


تحاول العديد من تطبيقات المكتبة تخفيف هذه المواقف من خلال دعم ذاكراتها الخاصة. مثال واحد هو التحميل الزائد من MemoryStream.ReadAsync . هذه العملية ، المقدمة في .NET Framework 4.5 ، تنتهي دائمًا بشكل متزامن ، لأن إنها مجرد قراءة من الذاكرة. إرجاع ReadAsync Task<int> حيث تمثل عدد صحيح عدد البايتات المقروءة. في كثير من الأحيان ، في التعليمات البرمجية ، يحدث موقف عند استخدام ReadAsync في حلقة. علاوة على ذلك ، إذا كانت هناك الأعراض التالية:


  • لا يتغير عدد البايتات المطلوبة لمعظم تكرارات الحلقة ؛
  • في معظم التكرارات ، يمكن لـ ReadAsync قراءة عدد البايتات المطلوب.

أي بالنسبة للمكالمات المتكررة ، يتم تشغيل ReadAsync بشكل متزامن وإرجاع كائن Task<int> ، مع نفس النتيجة من التكرار إلى التكرار. من المنطقي أن تقوم MemoryStream بتخزين آخر مهمة تم إكمالها بنجاح ، وبالنسبة لجميع المكالمات اللاحقة ، إذا كانت النتيجة الجديدة تطابق النتيجة السابقة ، فإنها تُرجع مثيلًا من ذاكرة التخزين المؤقت. إذا كانت النتيجة غير Task.FromResult ، Task.FromResult استخدام Task.FromResult لإنشاء مثيل جديد ، والذي بدوره يتم تخزينه مؤقتًا قبل العودة.


ولكن ، مع ذلك ، هناك العديد من الحالات التي يتم فيها فرض عملية لإنشاء كائنات Task<TResult> جديدة Task<TResult> ، حتى عند اكتمالها بشكل متزامن.


ValueTask <النتيجة> والإكمال المتزامن


كل هذا ، في النهاية ، كان بمثابة الحافز لإدخال نوع جديد من ValueTask<TResult> في .NET Core 2.0. أيضًا ، من خلال نظام الحزمة nuget-package.Threading.Tasks.Extensions ، تم توفير هذا النوع في إصدارات .NET الأخرى.


ValueTask<TResult> في .NET Core 2.0 TResult قادر على التفاف TResult أو Task<TResult> . هذا يعني أنه يمكن إرجاع الكائنات من هذا النوع من طريقة async . تكون الإضافة الأولى من مقدمة هذا النوع مرئية على الفور: إذا تم إكمال الأسلوب بنجاح وبشكل متزامن ، فليست هناك حاجة لإنشاء أي شيء على الكومة ، يكفي فقط لإنشاء مثيل لـ ValueTask<TResult> مع قيمة النتيجة. فقط في حالة خروج الطريقة بشكل غير متزامن ، نحتاج إلى إنشاء Task<TResult> . في هذه الحالة ، ValueTask<TResult> استخدام Task<TResult> عبر Task<TResult> . تم اتخاذ قرار لجعل ValueTask<TResult> قادرة على تجميع Task<TResult> بهدف التحسين: في حالة النجاح وفي حالة الفشل ، تنشئ الطريقة غير المتزامنة Task<TResult> ، من وجهة نظر تحسين الذاكرة ، من الأفضل تجميع Task<TResult> كائن Task<TResult> نفسه Task<TResult> من الاحتفاظ بحقول إضافية في ValueTask<TResult> لمختلف حالات الإكمال (على سبيل المثال ، لتخزين استثناء).


بالنظر إلى ما سبق ، لم تعد هناك حاجة للتخزين المؤقت في أساليب مثل MemoryStream.ReadAsync أعلاه ، ولكن بدلاً من ذلك يمكن تنفيذها على النحو التالي:


 public override ValueTask<int> ReadAsync(byte[] buffer, int offset, int count) { try { int bytesRead = Read(buffer, offset, count); return new ValueTask<int>(bytesRead); } catch (Exception e) { return new ValueTask<int>(Task.FromException<int>(e)); } } 

ValueTask <النتيجة> والانتهاء غير المتزامن


إن امتلاك القدرة على كتابة طرق غير متزامنة لا تتطلب تخصيصات ذاكرة إضافية للنتيجة ، مع الإكمال المتزامن ، يعد إضافة كبيرة بالفعل. كما ذكر أعلاه ، كان هذا هو الهدف الرئيسي لإدخال نوع القيمة الجديدة ValueTask<TResult> في .NET Core 2.0. تستخدم كل الطرق الجديدة التي من المتوقع استخدامها على "الطرق الساخنة" ValueTask<TResult> بدلاً من Task<TResult> كنوع الإرجاع. على سبيل المثال ، يؤدي التحميل الزائد الجديد لطريقة ReadAsync لـ Stream ، في .NET Core 2.1 (والذي يأخذ Memory<byte> بدلاً من byte[] كمعلمة) ، إلى إرجاع مثيل لـ ValueTask<int> . سمح هذا بتقليل عدد التخصيصات بشكل كبير عند العمل مع التدفقات (غالبًا ما تنتهي طريقة ReadAsync بشكل متزامن ، كما في المثال مع MemoryStream ).


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


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


لدعم القدرة على العمل مع هذه IValueTaskSource<TResult> تمت إضافة واجهة IValueTaskSource<TResult> إلى .NET Core 2.1 ، وتم توسيع بنية ValueTask<TResult> : الآن يمكن للكائنات من هذا النوع أن تلتف ليس فقط كائنات من TResult أو Task<TResult> ، ولكن أيضًا مثيلات IValueTaskSource<TResult> . توفر الواجهة الجديدة الوظائف الأساسية التي تسمح ValueTask<TResult> بالعمل مع IValueTaskSource<TResult> بنفس الطريقة التي تعمل بها Task<TResult> :


 public interface IValueTaskSource<out TResult> { ValueTaskSourceStatus GetStatus(short token); void OnCompleted( Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags); TResult GetResult(short token); } 

المقصود ValueTask<TResult>.IsCompleted/IsCompletedSuccessfully للاستخدام في ValueTask<TResult>.IsCompleted/IsCompletedSuccessfully - يسمح لك لمعرفة ما إذا كانت العملية مكتملة أم لا (بنجاح أم لا). OnCompleted استخدام ValueTask<TResult> في ValueTask<TResult> لتشغيل رد اتصال. يستخدم GetResult للحصول على النتيجة ، أو لرمي استثناء.


من غير المحتمل أن يحتاج معظم المطورين للتعامل مع IValueTaskSource<TResult> ، لأن الأساليب غير المتزامنة ، عند إرجاعها ، إخفاءها خلف ValueTask<TResult> . الواجهة نفسها مخصصة لأولئك الذين يطورون واجهات برمجة التطبيقات عالية الأداء ويسعون إلى تجنب العمل غير الضروري مع مجموعة.


في .NET Core 2.1 ، هناك العديد من الأمثلة لهذا النوع من API. الأكثر شهرة من هذه هي الأحمال الزائدة الجديدة من أساليب Socket.SendAsync و Socket.SendAsync . على سبيل المثال:


 public ValueTask<int> ReceiveAsync( Memory<byte> buffer, SocketFlags socketFlags, CancellationToken cancellationToken = default); 

يتم استخدام كائنات النوع ValueTask<int> كقيمة الإرجاع.
إذا تم إنهاء الطريقة بشكل متزامن ، فسوف تُرجع ValueTask<int> بالقيمة المقابلة:


 int result = …; return new ValueTask<int>(result); 

إذا اكتملت العملية بشكل غير متزامن ، فسيتم استخدام كائن تم تخزينه في ذاكرة التخزين المؤقت IValueTaskSource<TResult> واجهة IValueTaskSource<TResult> :


 IValueTaskSource<int> vts = …; return new ValueTask<int>(vts); 

يدعم تطبيق Socket كائنًا مؤقتًا واحدًا لاستلامه ، والآخر لإرسال البيانات ، طالما يتم استخدام كل منها دون منافسة (لا ، على سبيل المثال ، إرسال البيانات التنافسية). تعمل هذه الاستراتيجية على تقليل مقدار الذاكرة الإضافية المخصصة ، حتى في حالة التنفيذ غير المتزامن.
كان للتحسين الموضح لـ Socket في .NET Core 2.1 تأثير إيجابي على أداء NetworkStream . التحميل الزائد الخاص به هو أسلوب ReadAsync للفئة Stream :


 public virtual ValueTask<int> ReadAsync( Memory<byte> buffer, CancellationToken cancellationToken); 

فقط بتفويض العمل إلى الأسلوب Socket.ReceiveAsync . زيادة كفاءة طريقة المقبس ، من حيث العمل مع الذاكرة ، تزيد من كفاءة طريقة NetworkStream .


ValueTask غير العامة


في وقت سابق ، لاحظت عدة مرات أن الهدف الأصلي من ValueTask<T> ، في .NET Core 2.0 ، هو تحسين حالات الإكمال المتزامن للأساليب بنتيجة "غير فارغة". هذا يعني أنه لم تكن هناك حاجة إلى ValueTask غير المكتوبة: في حالات الإكمال المتزامن ، تستخدم الأساليب Task.CompletedTask عبر خاصية Task.CompletedTask ، كما يتم استلام وقت التشغيل لأساليب async Task بشكل ضمني.


ولكن مع ظهور القدرة على تجنب التخصيصات غير الضرورية والتنفيذ غير المتزامن ، أصبحت الحاجة مرة أخرى إلى ValueTask غير المكتوبة. لهذا السبب ، في .NET Core 2.1 ، قدمنا ValueTask غير ValueTask و IValueTaskSource . وهي نظائرها من الأنواع العامة المناظرة ، وتستخدم بالطريقة نفسها ، ولكن بالنسبة للطرق ذات العائد الفارغ ( void ).


تطبيق IValueTaskSource / IValueTaskSource <T>


لن يحتاج معظم المطورين إلى تطبيق هذه الواجهات. وتنفيذها ليست مهمة سهلة. إذا قررت أنك تحتاج إلى تنفيذها بنفسك ، فهناك داخل .NET Core 2.1 العديد من التطبيقات التي يمكن أن تعمل كأمثلة:



لتبسيط هذه المهام (تطبيقات IValueTaskSource / IValueTaskSource<T> ) ، نخطط لإدخال النوع ManualResetValueTaskSourceCore<TResult> في .NET Core 3.0. هذه البنية سوف تغلف كل المنطق اللازم. يمكن استخدام مثيل ManualResetValueTaskSourceCore<TResult> في كائن آخر يقوم بتطبيق IValueTaskSource<TResult> و / أو IValueTaskSource ، وتفويض معظم العمل إليه. يمكنك معرفة المزيد عن هذا على ttps: //github.com/dotnet/corefx/issues/32664.


النموذج الصحيح لاستخدام ValueTasks


حتى الفحص السريع ValueTask أن ValueTask و ValueTask<TResult> أكثر محدودية من Task and Task<TResult> . وهذا أمر طبيعي ، بل مرغوب فيه ، لأن هدفهم الرئيسي هو انتظار الانتهاء من التنفيذ غير المتزامن.


على وجه الخصوص ، تنشأ قيود كبيرة بسبب حقيقة أن ValueTask و ValueTask<TResult> يمكنهم تجميع الكائنات القابلة لإعادة الاستخدام. بشكل عام ، يجب تنفيذ العمليات التالية * NEVER عند استخدام ValueTask / ValueTask<TResult> * ( اسمحوا لي إعادة ValueTask<TResult> خلال "Never" *):


  • لا تستخدم ValueTask / ValueTask<TResult> مرارًا وتكرارًا

الدافع: لا تنتقل مثيلات Task<TResult> أبداً من الحالة "المكتملة" إلى الحالة "غير المكتملة" ، يمكننا استخدامها في انتظار النتيجة عدة مرات كما نريد - بعد الانتهاء ، سنحصل دائمًا على نفس النتيجة. على العكس من ذلك ، نظرًا لأن ValueTask / ValueTask<TResult> ، ValueTask<TResult> أن تعمل ValueTask<TResult> على كائنات قابلة لإعادة الاستخدام ، مما يعني أن حالتها يمكن أن تتغير ، لأن تتغير حالة الكائنات المعاد استخدامها بحكم التعريف - للانتقال من "مكتمل" إلى "غير مكتمل" والعكس بالعكس.


  • لا ValueTask أبدًا ValueTask / ValueTask&lt;TResult&gt; في وضع تنافسي.

الدافع: يتوقع كائن ملفوف العمل مع رد اتصال واحد فقط ، من مستهلك واحد في وقت واحد ، ومحاولة التنافس يمكن أن يؤدي بسهولة إلى ظروف سباق وأخطاء البرمجة الدقيقة. توقعات تنافسية ، وهذا هو واحد من الخيارات المذكورة أعلاه توقعات متعددة . لاحظ أن Task / Task<TResult> تسمح لأي عدد من التوقعات التنافسية.


  • لا تستخدم .GetAwaiter().GetResult() حتى تكتمل العملية .

الدافع: يجب ألا تدعم تطبيقات IValueTaskSource / IValueTaskSource<TResult> القفل حتى تكتمل العملية. الحجب ، في الواقع ، يؤدي إلى حالة سباق ، من غير المرجح أن يكون هذا هو السلوك المتوقع من جانب المستهلك. بينما تتيح لك Task / Task<TResult> القيام بذلك ، وبالتالي حظر مؤشر ترابط الاستدعاء حتى تكتمل العملية.


ولكن ماذا لو ، مع ذلك ، إذا كنت بحاجة إلى تنفيذ إحدى العمليات الموضحة أعلاه ، وطريقة استدعاء إرجاع مثيلات ValueTask / ValueTask<TResult> ؟ لمثل هذه الحالات ، ValueTask / ValueTask<TResult> طريقة .AsTask() . عن طريق استدعاء هذه الطريقة ، ستحصل على مثيل من Task / Task<TResult> ، ويمكنك بالفعل تنفيذ العملية اللازمة معها. إعادة استخدام الكائن الأصلي بعد استدعاء .AsTask() غير مسموح .


: ValueTask / ValueTask<TResult> , ( await ) (, .ConfigureAwait(false) ), .AsTask() , ValueTask / ValueTask<TResult> .


 // Given this ValueTask<int>-returning method… public ValueTask<int> SomeValueTaskReturningMethodAsync(); … // GOOD int result = await SomeValueTaskReturningMethodAsync(); // GOOD int result = await SomeValueTaskReturningMethodAsync().ConfigureAwait(false); // GOOD Task<int> t = SomeValueTaskReturningMethodAsync().AsTask(); // WARNING ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); ... // storing the instance into a local makes it much more likely it'll be misused, // but it could still be ok // BAD: awaits multiple times ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); int result = await vt; int result2 = await vt; // BAD: awaits concurrently (and, by definition then, multiple times) ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); Task.Run(async () => await vt); Task.Run(async () => await vt); // BAD: uses GetAwaiter().GetResult() when it's not known to be done ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); int result = vt.GetAwaiter().GetResult(); 

, "", , ( , ).


ValueTask / ValueTask<TResult> , . , IsCompleted true , ( , ), — false , IsCompletedSuccessfully true . " " , , , , , . await / .AsTask() .Result . , SocketsHttpHandler .NET Core 2.1, .ReadAsync , ValueTask<int> . , , , . , .. . لأن , , , , :


 int bytesRead; { ValueTask<int> readTask = _connection.ReadAsync(buffer); if (readTask.IsCompletedSuccessfully) { bytesRead = readTask.Result; } else { using (_connection.RegisterCancellation()) { bytesRead = await readTask; } } } 

, .. ValueTask<int> , .Result , await , .


API ValueTask / ValueTask<TResult>?


, . Task / ValueTask<TResult> .


, Task / Task<TResult> . , "" / , Task / Task<TResult> . , , ValueTask<TResult> Task<TResult> : , , await Task<TResult> ValueTask<TResult> . , (, API Task Task<bool> ), , , Task ( Task<bool> ). , ValueTask / ValueTask<TResult> . , async-, ValueTask / ValueTask<TResult> , .


, ValueTask / ValueTask<TResult> , :


  1. , API ,
  2. API ,
  3. , , , .

, abstract / virtual , , / ?


ما التالي؟


.NET, API, Task / Task<TResult> . , , API c ValueTask / ValueTask<TResult> , . IAsyncEnumerator<T> , .NET Core 3.0. IEnumerator<T> MoveNext , . — IAsyncEnumerator<T> MoveNextAsync . , Task<bool> , , . , , , ( ), , , await foreach -, , MoveNextAsync , ValueTask<bool> . , , , " " , . , C# , .


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


All Articles