[多线程]托管线程中的取消
.NET Framework 在协作取消异步操作或长时间运行的同步操作时使用统一的模型。 此模型基于被称为取消标记的轻量对象。 调用一个或多个可取消操作的对象(例如通过创建新线程或任务)将标记传递给每个操作。 单个操作反过来可将标记的副本传递给其他操作。 稍后,创建标记的对象可使用此标记请求停止执行操作内容。 只有发出请求的对象,才能发出取消请求,而每个侦听器负责侦听是否有请求,并及时适当地响应请求。
用于实现协作取消模型的常规模式是:
- 实例化 CancellationTokenSource 对象,此对象管理取消通知并将其发送给单个取消标记。
- 将 CancellationTokenSource.Token 属性返回的标记传递给每个侦听取消的任务或线程。
- 为每个任务或线程提供响应取消的机制。
- 调用 CancellationTokenSource.Cancel 方法以提供取消通知。
在以下示例中,请求对象创建 CancellationTokenSource 对象,然后传递其 Token 属性到可取消操作中。 接收请求的操作通过轮询监视标记的 IsCancellationRequested 属性的值。 值变为 true
后,侦听器可以适当方式终止操作。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 using System; 2 using System.Threading; 3 4 public class Example 5 { 6 public static void Main() 7 { 8 // Create the token source. 9 CancellationTokenSource cts = new CancellationTokenSource(); 10 11 // Pass the token to the cancelable operation. 12 ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token); 13 Thread.Sleep(2500); 14 15 // Request cancellation. 16 cts.Cancel(); 17 Console.WriteLine("Cancellation set in token source..."); 18 Thread.Sleep(2500); 19 // Cancellation should have happened, so call Dispose. 20 cts.Dispose(); 21 } 22 23 // Thread 2: The listener 24 static void DoSomeWork(object obj) 25 { 26 CancellationToken token = (CancellationToken)obj; 27 28 for (int i = 0; i < 100000; i++) { 29 if (token.IsCancellationRequested) 30 { 31 Console.WriteLine("In iteration {0}, cancellation has been requested...", 32 i + 1); 33 // Perform cleanup if necessary. 34 //... 35 // Terminate the operation. 36 break; 37 } 38 // Simulate some work. 39 Thread.SpinWait(500000); 40 } 41 } 42 } 43 // The example displays output like the following: 44 // Cancellation set in token source... 45 // In iteration 1430, cancellation has been requested...
取消任务及其子级:
- 创建并启动可取消任务。
- 将取消令牌传递给用户委托,并视需要传递给任务实例。
- 注意并响应和用户委托中的取消请求。
- (可选)注意已取消任务的调用线程。
调用线程不会强制结束任务,只会提示取消请求已发出。 如果任务已在运行,至于怎样才能注意请求并适当响应,取决于用户委托的选择。 如果取消请求在任务运行前发出,用户委托绝不会执行,任务对象的状态会转换为“已取消”。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 using System; 2 using System.Collections.Concurrent; 3 using System.Threading; 4 using System.Threading.Tasks; 5 6 public class Example 7 { 8 public static async Task Main() 9 { 10 var tokenSource = new CancellationTokenSource(); 11 var token = tokenSource.Token; 12 13 // Store references to the tasks so that we can wait on them and 14 // observe their status after cancellation. 15 Task t; 16 var tasks = new ConcurrentBag<Task>(); 17 18 Console.WriteLine("Press any key to begin tasks..."); 19 Console.ReadKey(true); 20 Console.WriteLine("To terminate the example, press 'c' to cancel and exit..."); 21 Console.WriteLine(); 22 23 // Request cancellation of a single task when the token source is canceled. 24 // Pass the token to the user delegate, and also to the task so it can 25 // handle the exception correctly. 26 t = Task.Run(() => DoSomeWork(1, token), token); 27 Console.WriteLine("Task {0} executing", t.Id); 28 tasks.Add(t); 29 30 // Request cancellation of a task and its children. Note the token is passed 31 // to (1) the user delegate and (2) as the second argument to Task.Run, so 32 // that the task instance can correctly handle the OperationCanceledException. 33 t = Task.Run(() => 34 { 35 // Create some cancelable child tasks. 36 Task tc; 37 for (int i = 3; i <= 10; i++) 38 { 39 // For each child task, pass the same token 40 // to each user delegate and to Task.Run. 41 tc = Task.Run(() => DoSomeWork(i, token), token); 42 Console.WriteLine("Task {0} executing", tc.Id); 43 tasks.Add(tc); 44 // Pass the same token again to do work on the parent task. 45 // All will be signaled by the call to tokenSource.Cancel below. 46 DoSomeWork(2, token); 47 } 48 }, token); 49 50 Console.WriteLine("Task {0} executing", t.Id); 51 tasks.Add(t); 52 53 // Request cancellation from the UI thread. 54 char ch = Console.ReadKey().KeyChar; 55 if (ch == 'c' || ch == 'C') 56 { 57 tokenSource.Cancel(); 58 Console.WriteLine("\nTask cancellation requested."); 59 60 // Optional: Observe the change in the Status property on the task. 61 // It is not necessary to wait on tasks that have canceled. However, 62 // if you do wait, you must enclose the call in a try-catch block to 63 // catch the TaskCanceledExceptions that are thrown. If you do 64 // not wait, no exception is thrown if the token that was passed to the 65 // Task.Run method is the same token that requested the cancellation. 66 } 67 68 try 69 { 70 await Task.WhenAll(tasks.ToArray()); 71 } 72 catch (OperationCanceledException) 73 { 74 Console.WriteLine($"\n{nameof(OperationCanceledException)} thrown\n"); 75 } 76 finally 77 { 78 tokenSource.Dispose(); 79 } 80 81 // Display status of all tasks. 82 foreach (var task in tasks) 83 Console.WriteLine("Task {0} status is now {1}", task.Id, task.Status); 84 } 85 86 static void DoSomeWork(int taskNum, CancellationToken ct) 87 { 88 // Was cancellation already requested? 89 if (ct.IsCancellationRequested) 90 { 91 Console.WriteLine("Task {0} was cancelled before it got started.", 92 taskNum); 93 ct.ThrowIfCancellationRequested(); 94 } 95 96 int maxIterations = 100; 97 98 // NOTE!!! A "TaskCanceledException was unhandled 99 // by user code" error will be raised here if "Just My Code" 100 // is enabled on your computer. On Express editions JMC is 101 // enabled and cannot be disabled. The exception is benign. 102 // Just press F5 to continue executing your code. 103 for (int i = 0; i <= maxIterations; i++) 104 { 105 // Do a bit of work. Not too much. 106 var sw = new SpinWait(); 107 for (int j = 0; j <= 100; j++) 108 sw.SpinOnce(); 109 110 if (ct.IsCancellationRequested) 111 { 112 Console.WriteLine("Task {0} cancelled", taskNum); 113 ct.ThrowIfCancellationRequested(); 114 } 115 } 116 } 117 } 118 // The example displays output like the following: 119 // Press any key to begin tasks... 120 // To terminate the example, press 'c' to cancel and exit... 121 // 122 // Task 1 executing 123 // Task 2 executing 124 // Task 3 executing 125 // Task 4 executing 126 // Task 5 executing 127 // Task 6 executing 128 // Task 7 executing 129 // Task 8 executing 130 // c 131 // Task cancellation requested. 132 // Task 2 cancelled 133 // Task 7 cancelled 134 // 135 // OperationCanceledException thrown 136 // 137 // Task 2 status is now Canceled 138 // Task 1 status is now RanToCompletion 139 // Task 8 status is now Canceled 140 // Task 7 status is now Canceled 141 // Task 6 status is now RanToCompletion 142 // Task 5 status is now RanToCompletion 143 // Task 4 status is now RanToCompletion 144 // Task 3 status is now RanToCompletion
对象取消:
如果需要对象取消机制,可以通过调用 CancellationToken.Register 方法将其基于操作取消机制,如以下示例所示。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 using System; 2 using System.Threading; 3 4 class CancelableObject 5 { 6 public string id; 7 8 public CancelableObject(string id) 9 { 10 this.id = id; 11 } 12 13 public void Cancel() 14 { 15 Console.WriteLine("Object {0} Cancel callback", id); 16 // Perform object cancellation here. 17 } 18 } 19 20 public class Example 21 { 22 public static void Main() 23 { 24 CancellationTokenSource cts = new CancellationTokenSource(); 25 CancellationToken token = cts.Token; 26 27 // User defined Class with its own method for cancellation 28 var obj1 = new CancelableObject("1"); 29 var obj2 = new CancelableObject("2"); 30 var obj3 = new CancelableObject("3"); 31 32 // Register the object's cancel method with the token's 33 // cancellation request. 34 token.Register(() => obj1.Cancel()); 35 token.Register(() => obj2.Cancel()); 36 token.Register(() => obj3.Cancel()); 37 38 // Request cancellation on the token. 39 cts.Cancel(); 40 // Call Dispose when we're done with the CancellationTokenSource. 41 cts.Dispose(); 42 } 43 } 44 // The example displays the following output: 45 // Object 3 Cancel callback 46 // Object 2 Cancel callback 47 // Object 1 Cancel callback
如果对象支持多个并发可取消操作,则将单独的标记作为输入传递给每个非重复的可取消操作。 这样,无需影响其他操作即可取消某项操作。
侦听和响应取消请求:
在用户委托中,可取消操作的实施者确定如何以响应取消请求来终止操作。 在很多情况下,用户委托只需执行全部所需清理,然后立即返回。但是,在更复杂的情况下,用户委托可能需要通知库代码已发生取消。 在这种情况下,终止操作的正确方式是委托调用 ThrowIfCancellationRequested 方法,这将引发 OperationCanceledException。 库代码可以在用户委托线程上捕获此异常,并检查异常的标记以确定异常是否表示协作取消或一些其他的异常情况。
- 通过轮询进行侦听
对于循环或递归的长时间运行的计算,可以通过定期轮询CancellationToken.IsCancellationRequested 属性的值来侦听取消请求。如果其值为 true
,则此方法应尽快清理并终止。
- 通过注册回调进行侦听
某些操作可能被阻止,导致其无法及时检查取消标记的值,这时可以注册在接收取消请求时取消阻止此方法的回调方法。Register 方法返回专用于此目的的 CancellationTokenRegistration 对象。以下示例演示了如何使用Register方法取消异步Web请求。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 using System; 2 using System.Net; 3 using System.Threading; 4 5 class Example 6 { 7 static void Main() 8 { 9 CancellationTokenSource cts = new CancellationTokenSource(); 10 11 StartWebRequest(cts.Token); 12 13 // cancellation will cause the web 14 // request to be cancelled 15 cts.Cancel(); 16 } 17 18 static void StartWebRequest(CancellationToken token) 19 { 20 WebClient wc = new WebClient(); 21 wc.DownloadStringCompleted += (s, e) => Console.WriteLine("Request completed."); 22 23 // Cancellation on the token will 24 // call CancelAsync on the WebClient. 25 token.Register(() => 26 { 27 wc.CancelAsync(); 28 Console.WriteLine("Request cancelled!"); 29 }); 30 31 Console.WriteLine("Starting request."); 32 wc.DownloadStringAsync(new Uri("http://www.contoso.com")); 33 } 34 }
- 通过使用等待句柄进行侦听
当可取消的操作可在等待同步基元(例如 System.Threading.ManualResetEvent 或 System.Threading.Semaphore)的同时进行阻止时),可使用 CancellationToken.WaitHandle 属性启用操作同时等待事件请求和取消请求。 取消标记的等待句柄将接收到响应取消请求的信号,并且此方法可使用 WaitAny 方法的返回值来确定它是否为发出信号的取消标记。 然后此操作可根据需要直接退出,或者引发 OperationCanceledException。
// Wait on the event if it is not signaled. int eventThatSignaledIndex = WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle }, new TimeSpan(0, 0, 20));
System.Threading.ManualResetEventSlim 和 System.Threading.SemaphoreSlim 都支持在其 Wait
方法中使用新的取消框架。 可以将 CancellationToken传递给方法,在取消请求发出后,事件就会唤醒并抛出 OperationCanceledException。
try { // mres is a ManualResetEventSlim mres.Wait(token); } catch (OperationCanceledException) { // Throw immediately to be responsive. The // alternative is to do one more item of work, // and throw on next iteration, because // IsCancellationRequested will be true. Console.WriteLine("The wait operation was canceled."); throw; } Console.Write("Working..."); // Simulating work. Thread.SpinWait(500000);
- 同时侦听多个标记
在某些情况下,侦听器可能需要同时侦听多个取消标记。 例如,除了在外部作为自变量传递到方法参数的标记以外,可取消操纵可能还必须监视内部取消标记。 为此,需创建可将两个或多个标记联接成一个标记的链接标记源,如以下示例所示。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public void DoWork(CancellationToken externalToken) 2 { 3 // Create a new token that combines the internal and external tokens. 4 this.internalToken = internalTokenSource.Token; 5 this.externalToken = externalToken; 6 7 using (CancellationTokenSource linkedCts = 8 CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken)) 9 { 10 try { 11 DoWorkInternal(linkedCts.Token); 12 } 13 catch (OperationCanceledException) { 14 if (internalToken.IsCancellationRequested) { 15 Console.WriteLine("Operation timed out."); 16 } 17 else if (externalToken.IsCancellationRequested) { 18 Console.WriteLine("Cancelling per user request."); 19 externalToken.ThrowIfCancellationRequested(); 20 } 21 } 22 } 23 }