[多线程]托管线程中的取消

  .NET Framework 在协作取消异步操作或长时间运行的同步操作时使用统一的模型。 此模型基于被称为取消标记的轻量对象。 调用一个或多个可取消操作的对象(例如通过创建新线程或任务)将标记传递给每个操作。 单个操作反过来可将标记的副本传递给其他操作。 稍后,创建标记的对象可使用此标记请求停止执行操作内容。 只有发出请求的对象,才能发出取消请求,而每个侦听器负责侦听是否有请求,并及时适当地响应请求。

  用于实现协作取消模型的常规模式是:

  1. 实例化 CancellationTokenSource 对象,此对象管理取消通知并将其发送给单个取消标记。
  2. 将 CancellationTokenSource.Token 属性返回的标记传递给每个侦听取消的任务或线程。
  3. 为每个任务或线程提供响应取消的机制。
  4. 调用 CancellationTokenSource.Cancel 方法以提供取消通知。

  在以下示例中,请求对象创建 CancellationTokenSource 对象,然后传递其 Token 属性到可取消操作中。 接收请求的操作通过轮询监视标记的 IsCancellationRequested 属性的值。 值变为 true 后,侦听器可以适当方式终止操作。

 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...
View Code

  取消任务及其子级:

  1.   创建并启动可取消任务。
  2.   将取消令牌传递给用户委托,并视需要传递给任务实例。
  3.   注意并响应和用户委托中的取消请求。
  4.   (可选)注意已取消任务的调用线程。

  调用线程不会强制结束任务,只会提示取消请求已发出。 如果任务已在运行,至于怎样才能注意请求并适当响应,取决于用户委托的选择。 如果取消请求在任务运行前发出,用户委托绝不会执行,任务对象的状态会转换为“已取消”。

  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("
Task 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($"
{nameof(OperationCanceledException)} thrown
");
 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
View Code

  对象取消:

  如果需要对象取消机制,可以通过调用 CancellationToken.Register 方法将其基于操作取消机制,如以下示例所示。

 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
View Code

  如果对象支持多个并发可取消操作,则将单独的标记作为输入传递给每个非重复的可取消操作。 这样,无需影响其他操作即可取消某项操作。

  侦听和响应取消请求:

   在用户委托中,可取消操作的实施者确定如何以响应取消请求来终止操作。 在很多情况下,用户委托只需执行全部所需清理,然后立即返回。但是,在更复杂的情况下,用户委托可能需要通知库代码已发生取消。 在这种情况下,终止操作的正确方式是委托调用 ThrowIfCancellationRequested 方法,这将引发 OperationCanceledException。 库代码可以在用户委托线程上捕获此异常,并检查异常的标记以确定异常是否表示协作取消或一些其他的异常情况。

  • 通过轮询进行侦听

  对于循环或递归的长时间运行的计算,可以通过定期轮询CancellationToken.IsCancellationRequested 属性的值来侦听取消请求。如果其值为 true,则此方法应尽快清理并终止。

  • 通过注册回调进行侦听

  某些操作可能被阻止,导致其无法及时检查取消标记的值,这时可以注册在接收取消请求时取消阻止此方法的回调方法。Register 方法返回专用于此目的的 CancellationTokenRegistration 对象。以下示例演示了如何使用Register方法取消异步Web请求。

 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 }
View Code
  • 通过使用等待句柄进行侦听

  当可取消的操作可在等待同步基元(例如 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);
  • 同时侦听多个标记

  在某些情况下,侦听器可能需要同时侦听多个取消标记。 例如,除了在外部作为自变量传递到方法参数的标记以外,可取消操纵可能还必须监视内部取消标记。 为此,需创建可将两个或多个标记联接成一个标记的链接标记源,如以下示例所示。

 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 }
View Code
 
 
 
原文地址:https://www.cnblogs.com/amytal/p/11720871.html