A .NET Programmer's Guide to CancellationToken

เผยแพร่แล้ว: 2022-08-23

บางครั้งการยกเลิกเป็นสิ่งที่ดี ในหลายโครงการ .NET ของฉัน ฉันมีแรงจูงใจมากมายที่จะยกเลิกกระบวนการทั้งภายในและภายนอก Microsoft ได้เรียนรู้ว่านักพัฒนากำลังเข้าใกล้กรณีการใช้งานทั่วไปนี้ในการใช้งานที่ซับซ้อนหลากหลาย และตัดสินใจว่าจะต้องมีวิธีที่ดีกว่านี้ ดังนั้น รูปแบบการสื่อสารการยกเลิกทั่วไปจึงถูกนำมาใช้เป็น CancellationToken ซึ่งสร้างขึ้นโดยใช้โครงสร้างการสื่อสารแบบมัลติเธรดระดับล่างและการสื่อสารระหว่างกระบวนการ จากการวิจัยเบื้องต้นของฉันเกี่ยวกับรูปแบบนี้ และหลังจากได้ขุดค้นซอร์สโค้ด .NET จริงสำหรับการใช้งานของ Microsoft แล้ว ฉันพบว่า CancellationToken สามารถแก้ปัญหาชุดใหญ่ได้: การสมัครรับข้อมูลสถานะการรันของแอปพลิเคชัน การหมดเวลาการดำเนินการโดยใช้ ทริกเกอร์ และการสื่อสารระหว่างกระบวนการทั่วไปผ่านแฟล็ก

กรณีใช้โทเค็นการยกเลิกที่ตั้งใจไว้

CancellationToken ถูกนำมาใช้ใน .NET 4 เพื่อเพิ่มประสิทธิภาพและสร้างมาตรฐานให้กับโซลูชันที่มีอยู่สำหรับการยกเลิกการดำเนินการ มีสี่วิธีทั่วไปในการจัดการการยกเลิกที่ภาษาโปรแกรมยอดนิยมมักจะนำไปใช้:

ฆ่า บอกอย่าใช้ไม่มีคำตอบ ถามอย่างสุภาพและยอมรับการปฏิเสธ ตั้งธงอย่างสุภาพ ให้มันโพล ถ้าต้องการ
เข้าใกล้ หยุดยาก; แก้ไขความไม่สอดคล้องกันในภายหลัง บอกให้หยุดแต่ปล่อยให้มันทำความสะอาด คำขอร้องที่ตรงไปตรงมาแต่อ่อนโยนให้หยุด ขอให้หยุดแต่อย่าฝืน
สรุป เส้นทางที่แน่นอนสู่การทุจริตและความเจ็บปวด ยอมให้จุดหยุดนิ่งแต่ต้องหยุด อนุญาตจุดหยุดที่สะอาด แต่คำขอยกเลิกอาจถูกเพิกเฉย ขอยกเลิกผ่านแฟล็ก
Pthreads pthread_kill ,
pthread_cancel (อะซิงโครนัส)
pthread_cancel (โหมดรอการตัดบัญชี) n/a ผ่านธง
.สุทธิ Thread.Abort n/a Thread.Interrupt ผ่านธงใน CancellationToken
Java กระทู้. Thread.destroy ,
Thread.stop
n/a Thread.interrupt ผ่านแฟล็กหรือ Thread.interrupted
Python PyThreadState_SetAsyncExc n/a asyncio.Task.cancel ผ่านธง
คำแนะนำ ยอมรับไม่ได้; หลีกเลี่ยงวิธีนี้ ยอมรับได้ โดยเฉพาะอย่างยิ่งเมื่อภาษาไม่รองรับข้อยกเว้นหรือการคลี่คลาย ยอมรับได้หากภาษารองรับ ดีกว่า แต่มีความพยายามเป็นกลุ่มมากขึ้น
สรุปวิธีการยกเลิกและตัวอย่างภาษา

CancellationToken อยู่ในหมวดหมู่สุดท้ายซึ่งการสนทนาการยกเลิกเป็นแบบร่วมมือ

หลังจากที่ Microsoft เปิดตัว CancellationToken ชุมชนการพัฒนาก็นำมันมาใช้อย่างรวดเร็ว โดยเฉพาะอย่างยิ่งเนื่องจาก .NET API หลักๆ จำนวนมากได้รับการอัปเดตเพื่อใช้โทเค็นเหล่านี้โดยกำเนิด ตัวอย่างเช่น เริ่มต้นด้วย ASP.NET Core 2.0 การดำเนินการสนับสนุนพารามิเตอร์ CancellationToken ที่เป็นตัวเลือกซึ่งอาจส่งสัญญาณหากคำขอ HTTP ถูกปิด ทำให้สามารถยกเลิกการดำเนินการใดๆ และหลีกเลี่ยงการใช้ทรัพยากรโดยไม่จำเป็น

หลังจากเจาะลึกลงไปใน .NET codebase เป็นที่ชัดเจนว่าการใช้งานของ CancellationToken ไม่ได้จำกัดอยู่แค่การยกเลิกเท่านั้น

โทเค็นการยกเลิกภายใต้กล้องจุลทรรศน์

เมื่อพิจารณาการใช้งานของ CancellationToken อย่างใกล้ชิดมากขึ้น เราจะเห็นว่าเป็นเพียงแฟล็กธรรมดา (เช่น ManualResetEvent ) และโครงสร้างพื้นฐานที่สนับสนุนที่ให้ความสามารถในการตรวจสอบและเปลี่ยนแฟล็กนั้น ยูทิลิตีหลักของ CancellationToken อยู่ในชื่อ ซึ่งแนะนำว่านี่เป็นวิธีทั่วไปในการยกเลิกการดำเนินการ ทุกวันนี้ ไลบรารี แพ็คเกจ หรือเฟรมเวิร์ก .NET ใดๆ ที่มีการดำเนินการแบบอะซิงโครนัสหรือระยะยาวอนุญาตให้ยกเลิกผ่านโทเค็นเหล่านี้ได้

โทเค็นการ CancellationToken อาจถูกเรียกใช้โดยการตั้งค่าสถานะด้วยตนเองเป็น "จริง" หรือตั้งโปรแกรมให้เปลี่ยนเป็น "จริง" หลังจากผ่านช่วงเวลาหนึ่งไปแล้ว ไม่ว่าจะทริกเกอร์ CancellationToken อย่างไร รหัสไคลเอ็นต์ที่กำลังตรวจสอบโทเค็นนี้อาจกำหนดค่าสถานะโทเค็นโดยใช้วิธีใดวิธีหนึ่งจากสามวิธีต่อไปนี้

  • การใช้ WaitHandle
  • การสำรวจธงของ CancellationToken
  • แจ้งรหัสลูกค้าเมื่อมีการอัพเดตสถานะของแฟล็กผ่านการสมัครสมาชิกแบบเป็นโปรแกรม

หลังจากการวิจัยเพิ่มเติมใน .NET codebase พบว่าทีม .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 สามารถใช้เพื่อเริ่มกระบวนการแทนที่จะหยุดได้อย่างไร

พิจารณาสิ่งต่อไปนี้:

  1. สร้างคลาส RandomWorker
  2. RandomWorker ควรมีเมธอด DoWorkAsync ที่ทำงานแบบสุ่ม
  3. เมธอด 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 ที่กว้างขวาง

ตัวอย่างเหล่านี้แสดงให้เห็นว่า CancellationToken มีกล่องเครื่องมือของโซลูชันที่เป็นประโยชน์นอกกรณีการใช้งานที่ตั้งใจไว้อย่างไร เครื่องมือมีประโยชน์ในหลาย ๆ สถานการณ์ที่เกี่ยวข้องกับการสื่อสารตามแฟล็กระหว่างกระบวนการ ไม่ว่าเราจะต้องเผชิญกับการหมดเวลา การแจ้งเตือน หรือเหตุการณ์ที่เกิดขึ้นเพียงครั้งเดียว เราก็สามารถถอยกลับไปใช้งานที่สง่างามและทดสอบโดย Microsoft ได้

จากบนลงล่าง คำว่า "Gold" (สีทอง), "Microsoft" และ "Partner" (สีดำทั้งคู่) จะปรากฏขึ้นตามด้วยโลโก้ Microsoft
ในฐานะ Microsoft Gold Partner Toptal เป็นเครือข่ายผู้เชี่ยวชาญของ Microsoft ที่ยอดเยี่ยม สร้างทีมที่มีประสิทธิภาพสูงด้วยผู้เชี่ยวชาญที่คุณต้องการ ทุกที่และทุกเวลาที่คุณต้องการ!

อ่านเพิ่มเติมในบล็อก Toptal Engineering:

  • .NET Core: ก้าวไกลและโอเพ่นซอร์ส Microsoft อะไรทำให้คุณใช้เวลานานขนาดนี้!
  • การสร้าง ASP.NET Web API ด้วย ASP.NET Core
  • วิธีการ Boot-strap และสร้าง .NET Projects