دليل. NET للمبرمجين للإلغاء
نشرت: 2022-08-23أحيانًا يكون الإلغاء أمرًا جيدًا. في العديد من مشاريع .NET الخاصة بي ، كان لدي الكثير من الحافز لإلغاء كل من العمليات الداخلية والخارجية. علمت Microsoft أن المطورين كانوا يقتربون من حالة الاستخدام الشائعة هذه في مجموعة متنوعة من التطبيقات المعقدة وقررت أنه يجب أن يكون هناك طريقة أفضل. وبالتالي ، تم تقديم نمط اتصال شائع للإلغاء باسم CancellationToken
، والذي تم إنشاؤه باستخدام بنيات اتصال متعددة الخيوط وعمليات اتصال بين العمليات منخفضة المستوى. كجزء من بحثي الأولي حول هذا النمط - وبعد التنقيب في كود مصدر .NET الفعلي لتطبيق Microsoft - وجدت أن CancellationToken
يمكنه حل مجموعة أكبر من المشكلات: الاشتراكات في حالات تشغيل التطبيقات ، وتوقيت العمليات باستخدام مختلف المشغلات والاتصالات العامة بين العمليات عبر الأعلام.
واقعة الاستخدام المقصودة للإلغاء
تم تقديم CancellationToken
في .NET 4 كوسيلة لتحسين وتوحيد الحلول الحالية لإلغاء العمليات. هناك أربع طرق عامة للتعامل مع الإلغاء تميل لغات البرمجة الشائعة إلى تنفيذها:
قتل | قل ، لا تأخذ أي إجابة | اسأل بأدب واقبل الرفض | ضع العلم بأدب ، دعه يستقصي إذا أراد | |
---|---|---|---|---|
يقترب | توقف الصعب؛ حل التناقضات لاحقًا | قل له أن يتوقف ولكن دعه ينظف الأشياء | طلب مباشر ولكن لطيف للتوقف | اطلب منه التوقف ، لكن لا تجبره |
ملخص | طريق مؤكد إلى الفساد والألم | يسمح بنقاط توقف نظيفة ولكن يجب أن يتوقف | يسمح بنقاط توقف نظيفة ، ولكن قد يتم تجاهل طلب الإلغاء | يُطلب الإلغاء من خلال العلم |
باتريدس | pthread_kill ،pthread_cancel (غير متزامن) | pthread_cancel (الوضع المؤجل) | غير متوفر | من خلال العلم |
.صافي | Thread.Abort | غير متوفر | Thread.Interrupt | من خلال علامة في CancellationToken |
جافا | Thread.destroy .Thread.stop | غير متوفر | Thread.interrupt | من خلال علم أو Thread.interrupted |
بايثون | PyThreadState_SetAsyncExc | غير متوفر | asyncio.Task.cancel | من خلال العلم |
إرشاد | غير مقبول؛ تجنب هذا النهج | مقبول ، خاصةً عندما لا تدعم اللغة الاستثناءات أو فك الارتباط | مقبول إذا كانت اللغة تدعمه | أفضل ، ولكن أكثر من جهد جماعي |
يقع CancellationToken
في الفئة النهائية ، حيث تكون محادثة الإلغاء تعاونية.
بعد أن قدمت Microsoft CancellationToken
، تبناه مجتمع التطوير سريعًا ، لا سيما لأنه تم تحديث العديد من واجهات برمجة تطبيقات .NET الرئيسية لاستخدام هذه الرموز المميزة في الأصل. على سبيل المثال ، بدءًا من ASP.NET Core 2.0 ، تدعم الإجراءات معلمة CancellationToken
اختيارية قد تشير إلى ما إذا تم إغلاق طلب HTTP ، مما يسمح بإلغاء أي عملية وبالتالي تجنب الاستخدام غير الضروري للموارد.
بعد الغوص العميق في قاعدة بيانات .NET البرمجية ، أصبح من الواضح أن استخدام CancellationToken
لا يقتصر على الإلغاء.
الإلغاء: تحت المجهر
عند النظر عن كثب إلى تنفيذ CancellationToken
، نرى أنه مجرد علامة بسيطة (على سبيل المثال ، ManualResetEvent
) والبنية التحتية الداعمة التي توفر القدرة على مراقبة هذه العلامة وتغييرها. الأداة الرئيسية لـ CancellationToken
هي اسمها ، مما يشير إلى أن هذه هي الطريقة الشائعة لإلغاء العمليات. في الوقت الحاضر ، تسمح أي مكتبة أو حزمة أو إطار عمل يحتوي على عمليات غير متزامنة أو طويلة التشغيل بالإلغاء من خلال هذه الرموز المميزة.
يمكن تشغيل CancellationToken
إما عن طريق تعيين علامته يدويًا على "true" أو برمجتها لتغييرها إلى "true" بعد انقضاء فترة زمنية معينة. بغض النظر عن كيفية تشغيل CancellationToken
، قد يحدد رمز العميل الذي يراقب هذا الرمز قيمة علامة الرمز المميز من خلال إحدى الطرق الثلاث:
- باستخدام
WaitHandle
- استقصاء علم
CancellationToken
- إبلاغ رمز العميل عند تحديث حالة العلم من خلال اشتراك برمجي
بعد إجراء مزيد من البحث في قاعدة بيانات .NET البرمجية ، أصبح من الواضح أن فريق .NET وجد أن CancellationTokens
مفيدة في السيناريوهات الأخرى غير المرتبطة بالإلغاء. دعنا نستكشف بعض حالات الاستخدام المتقدمة وغير المناسبة للعلامة التجارية ، والتي تمكّن مطوري C # بالتنسيق متعدد الخيوط والعمليات البينية لتبسيط المواقف المعقدة.
الإلغاء الرموز للأحداث المتقدمة
عند كتابة تطبيقات ASP.NET Core ، نحتاج أحيانًا إلى معرفة وقت بدء تطبيقنا ، أو نحتاج إلى إدخال الكود الخاص بنا في عملية إيقاف تشغيل المضيف. في هذه الحالات ، نستخدم واجهة IHostApplicationLifetime
(سابقًا IApplicationLifetime
). تستخدم هذه الواجهة (من مستودع .NET Core) CancellationToken
للإبلاغ عن ثلاثة أحداث رئيسية: ApplicationStarted
ApplicationStopping
ApplicationStopped
:
namespace Microsoft.Extensions.Hosting { /// <summary> /// Allows consumers to be notified of application lifetime events. /// This interface is not intended to be user-replaceable. /// </summary> public interface IHostApplicationLifetime { /// <summary> /// Triggered when the application host has fully started. /// </summary> CancellationToken ApplicationStarted { get; } /// <summary> /// Triggered when the application host is starting a graceful shutdown. /// Shutdown will block until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopping { get; } /// <summary> /// Triggered when the application host has completed a graceful shutdown. /// The application will not exit until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopped { get; } /// <summary> /// Requests termination of the current application. /// </summary> void StopApplication(); } }
للوهلة الأولى ، قد يبدو أن CancellationToken
لا ينتمي هنا ، خاصة أنه يتم استخدامه كأحداث. ومع ذلك ، يكشف مزيد من الفحص أن هذه الرموز المميزة مناسبة تمامًا:
- إنها مرنة ، مما يسمح بطرق متعددة لعميل الواجهة للاستماع إلى هذه الأحداث.
- هم آمنون من الخيط خارج منطقة الجزاء.
- يمكن إنشاؤها من مصادر مختلفة عن طريق الجمع بين
CancellationToken
s.
على الرغم من أن CancellationToken
ليست مثالية لكل حدث يحتاج إليه ، إلا أنها مثالية للأحداث التي تحدث مرة واحدة فقط ، مثل بدء التطبيق أو إيقافه.
الإلغاء
بشكل افتراضي ، يمنحنا ASP.NET وقتًا قصيرًا جدًا لإيقاف التشغيل. في تلك الحالات التي نريد فيها المزيد من الوقت ، يسمح لنا استخدام فئة HostOptions
بتغيير قيمة المهلة هذه. تحتها ، يتم تغليف قيمة المهلة هذه في CancellationToken
وإدخالها في العمليات الفرعية الأساسية.
تُعد طريقة IHostedService
في StopAsync
مثالًا رائعًا على هذا الاستخدام:
namespace Microsoft.Extensions.Hosting { /// <summary> /// Defines methods for objects that are managed by the host. /// </summary> public interface IHostedService { /// <summary> /// Triggered when the application host is ready to start the service. /// </summary> /// <param name="cancellationToken">Indicates that the start /// process has been aborted.</param> Task StartAsync(CancellationToken cancellationToken); /// <summary> /// Triggered when the application host is performing a graceful shutdown. /// </summary> /// <param name="cancellationToken">Indicates that the shutdown /// process should no longer be graceful.</param> Task StopAsync(CancellationToken cancellationToken); } }
كما هو واضح في تعريف واجهة IHostedService
، تأخذ طريقة StopAsync
معلمة CancellationToken
واحدة. يوضح التعليق المرتبط بهذه المعلمة بوضوح أن نية Microsoft الأولية لـ CancellationToken
كانت بمثابة آلية مهلة بدلاً من عملية إلغاء.
في رأيي ، إذا كانت هذه الواجهة موجودة قبل وجود CancellationToken
، فمن الممكن أن يكون هذا معلمة TimeSpan
- للإشارة إلى المدة التي سُمح خلالها لعملية الإيقاف. من واقع خبرتي ، يمكن تحويل سيناريوهات المهلة دائمًا إلى CancellationToken
مع أداة إضافية رائعة.
في الوقت الحالي ، دعنا ننسى أننا نعرف كيف تم تصميم طريقة StopAsync
بدلاً من ذلك في كيفية تصميم عقد هذه الطريقة. أولاً ، دعنا نحدد المتطلبات:
- يجب أن تحاول طريقة
StopAsync
إيقاف الخدمة. - يجب أن يكون لطريقة
StopAsync
حالة توقف رشيقة. - بغض النظر عما إذا كانت حالة الإيقاف الرشيقة قد تحققت ، يجب أن يكون للخدمة المستضافة أقصى وقت للتوقف ، كما هو محدد بواسطة معلمة المهلة الخاصة بنا.
من خلال وجود طريقة StopAsync
بأي شكل من الأشكال ، فإننا نلبي الشرط الأول. المتطلبات المتبقية صعبة. يلبي CancellationToken
هذه المتطلبات تمامًا باستخدام أداة اتصال قياسية قائمة على علامة .NET لتمكين المحادثة.
الإلغاء يعتبر بمثابة آلية إعلام
أكبر سر وراء CancellationToken
هو أنه مجرد علم. دعنا نوضح كيف يمكن استخدام CancellationToken
لبدء العمليات بدلاً من إيقافها.
ضع في اعتبارك ما يلي:
- قم بإنشاء فئة
RandomWorker
. - يجب أن يكون لدى
RandomWorker
طريقةDoWorkAsync
التي تنفذ بعض الأعمال العشوائية. - يجب أن يسمح أسلوب
DoWorkAsync
للمتصل بتحديد وقت بدء العمل.
public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync() { for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }
تستوفي الفئة المذكورة أعلاه الشرطين الأولين ، وتترك لنا الدرجة الثالثة. هناك العديد من الواجهات البديلة التي يمكننا استخدامها لتشغيل عاملنا ، مثل فترة زمنية أو علامة بسيطة:
# With a time span Task DoWorkAsync(TimeSpan startAfter); # Or a simple flag bool ShouldStart { get; set; } Task DoWorkAsync();
هاتان الطريقتان جيدتان ، ولكن لا يوجد شيء أنيق مثل استخدام CancellationToken
:
public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync(CancellationToken startToken) { startToken.WaitHandle.WaitOne(); for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }
يوضح نموذج رمز العميل هذا قوة هذا التصميم:
using System; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace CancelToStart { public class Program { static void Main(string[] args) { CancellationTokenSource startCts = new CancellationTokenSource(); startCts.CancelAfter(TimeSpan.FromSeconds(10)); var tasks = Enumerable.Range(0, 10) .Select(i => new RandomWorker(i)) .Select(worker => worker.DoWorkAsync(startCts.Token)) .ToArray(); Task.WaitAll(tasks, CancellationToken.None); } } }
سيقوم CancellationTokenSource
بإنشاء CancellationToken
خلف الكواليس وتنسيق تشغيل جميع العمليات المرتبطة. في هذه الحالة ، فإن العملية المرتبطة هي RandomWorker
، والتي تنتظر أن تبدأ. يتيح لنا هذا الأسلوب الاستفادة من أمان مؤشر الترابط المخبوز في تطبيق CancellationToken
الافتراضي.
صندوق أدوات موسع للإلغاء
توضح هذه الأمثلة كيف يوفر CancellationToken
مجموعة أدوات من الحلول المفيدة خارج حالة الاستخدام المقصودة. يمكن أن تكون الأدوات مفيدة في العديد من السيناريوهات التي تتضمن اتصالًا قائمًا على علم العمليات البينية. سواء واجهتنا مهلات أو إخطارات أو أحداث لمرة واحدة ، يمكننا الرجوع إلى هذا التطبيق الأنيق الذي تم اختباره من قبل Microsoft.
مزيد من القراءة على مدونة Toptal Engineering:
- NET Core: الذهاب إلى البرية والمفتوحة المصدر. مايكروسوفت ، ما الذي أخذك كل هذا الوقت ؟!
- بناء واجهة برمجة تطبيقات ويب ASP.NET مع ASP.NET Core
- كيفية إنشاء حزام التمهيد وإنشاء مشاريع .NET