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
สามารถใช้เพื่อเริ่มกระบวนการแทนที่จะหยุดได้อย่างไร
พิจารณาสิ่งต่อไปนี้:
- สร้างคลาส
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 ที่กว้างขวาง
ตัวอย่างเหล่านี้แสดงให้เห็นว่า CancellationToken
มีกล่องเครื่องมือของโซลูชันที่เป็นประโยชน์นอกกรณีการใช้งานที่ตั้งใจไว้อย่างไร เครื่องมือมีประโยชน์ในหลาย ๆ สถานการณ์ที่เกี่ยวข้องกับการสื่อสารตามแฟล็กระหว่างกระบวนการ ไม่ว่าเราจะต้องเผชิญกับการหมดเวลา การแจ้งเตือน หรือเหตุการณ์ที่เกิดขึ้นเพียงครั้งเดียว เราก็สามารถถอยกลับไปใช้งานที่สง่างามและทดสอบโดย Microsoft ได้
อ่านเพิ่มเติมในบล็อก Toptal Engineering:
- .NET Core: ก้าวไกลและโอเพ่นซอร์ส Microsoft อะไรทำให้คุณใช้เวลานานขนาดนี้!
- การสร้าง ASP.NET Web API ด้วย ASP.NET Core
- วิธีการ Boot-strap และสร้าง .NET Projects