Skip to content

C#多线程

1963字约7分钟

2025-09-09

C# 中的任务取消

MSDN的示例

1.取消Task c#中的任务取消是协作式的,什么是协作式的呢。就是说调用方和被调用方协商着来,不能说你想取消就取消。首先被调用方需要接受一个 CancellationToken 类型的参数,外部通过CancellationTokenSource 类型的实例调用Cancel来请求取消,此时该实例内的Token会将属性IsCancellationRequested 设置为true。被调用函数内部使用此通知来自己决定是否取消,被调用函数的函数签名中完全可以不接受CancellationToken类型的参数,或者完全不理会取消请求。在需要取消的场合,通过判断IsCancellationRequested,调用ct.ThrowIfCancellationRequested();来抛出异常以此取消任务,当然调用方要try-catch处理异常

这是一个示例 这段代码犯了一个经典错误 参考这篇文章

public class MThread
{

    //学习任务取消CancellationTokenSource
    public static async Task MThreadCancelTest()
    {
        var cts = new System.Threading.CancellationTokenSource();
        var token = cts.Token;
        var task = Task.Run(() => MTaskWillbeCanceled(token,10), token);
        //等待3秒后取消任务
        await Task.Delay(3000);
        cts.Cancel();
        try
        {
            await task;
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Task was canceled.");
        }
        catch (AggregateException ae)
        {
            foreach (var e in ae.InnerExceptions)
            {
                if (e is OperationCanceledException)
                {
                    Console.WriteLine("Task was canceled.");
                }
                else
                {
                    Console.WriteLine($"Task failed: {e.Message}");
                }
            }
        }
        finally
        {
            cts.Dispose();
        }


    }

    //协作式取消 不是说你想取消就取消 需要任务本身配合检查取消状态,需要函数签名中包含
    // CancellationToken参数 通过抛异常的方式取消任务
    private static async void MTaskWillbeCanceled(CancellationToken ct,int sencond)
    {
        if (ct.IsCancellationRequested)
        {
            Console.WriteLine("Task was canceled before it started.");
            ct.ThrowIfCancellationRequested();
        }
        for(int i = 0; i < sencond; i++)
        {
            // 模拟一些工作
            await Task.Delay(1000);
            Console.WriteLine($"Working... {i + 1} seconds elapsed.");
            // 定期检查取消状态
            if (ct.IsCancellationRequested)
            {
                Console.WriteLine("Task was canceled during execution.");
                ct.ThrowIfCancellationRequested();
            }
        }

    }
}

tips msdn的说明 下面两个语句是同义 参考runtime里的实现

ct.ThrowIfCancellationRequested();

if (token.IsCancellationRequested)   
    throw new OperationCanceledException(token);

CancellationTokenSource和CancellationToken 一些成员函数的

Register(Action)	
注册一个将在取消此 CancellationToken 时调用的委托。

Register(Action, Boolean)	
注册一个将在取消此 CancellationToken 时调用的委托。

Register(Action<Object,CancellationToken>, Object)	
注册取消此 CancellationToken 时将调用的委托。

Register(Action<Object>, Object)	
注册一个将在取消此 CancellationToken 时调用的委托。

Register(Action<Object>, Object, Boolean)	
注册一个将在取消此 CancellationToken 时调用的委托



//CTS
//这个重载第一个回调Register 注册的函数 抛出的异常不会阻止后续回调函数调用
public void Cancel();

//指示 回调抛出的异常是否需要立即传播
public void Cancel(bool throwOnFirstException);

//异步的取消,同时注册的回调也是异步调用
public System.Threading.Tasks.Task CancelAsync();

Ai说CancellationToken 没有一个 Register<T>(Action<T>, T) 这样重载的成员函数是因为泛型 增加内存和jit编译,该函数主要给引用类型用的;装箱的性能损失可以接受;api灵活——在ui场景用(我意淫的);历史原因——CancellationToken 是在 .NET Framework 4.0 中引入的,当时泛型的使用还没有像现在这样普遍。为了保持向后兼容性,API 的设计可能更倾向于非泛型的实现。

1.Register 注册回调函数在后进先出的队列中(底层实现是双向链表),后注册的函数在取消发生时最先调用。

2.两个Cancel的重载 无参和Cancel(false)的版本 在回调的调用链发生异常时都不会立即抛出异常;阻止注册的函数的调用。所以这重载在回调函数抛出异常时都将异常合并到一个AggregateException类里再抛出

 public static async Task CTSTest()
 {
     var cts = new CancellationTokenSource();
     var token = cts.Token;
     try
     {
         token.Register(() => {
         Console.WriteLine("Cancellation requested,首个注册的回调函数.");
         throw new ArgumentOutOfRangeException("Amount of shapes must be positive."); });
     token.Register(() => {
         Console.WriteLine("第二个注册的回调函数.");
         throw new StackOverflowException("Stack overflow in cancellation callback.");
     });
    
     Console.WriteLine($"IsCancellationRequested: {token.IsCancellationRequested}");
     Console.WriteLine($"CanBeCanceled: {token.CanBeCanceled}");
     // 注册取消回调
     token.Register(() => { Console.WriteLine("最后一个注册,被取消后首个调用的回调函数.");
         throw new NullReferenceException("空引用了");   });
     
     
         var task = Task.Run(() =>
         {
             for (int i = 0; i < 10; i++)
             {
                 token.ThrowIfCancellationRequested();
                 Console.WriteLine($"Doing work... {i + 1}");
                 Thread.Sleep(1);
             }
             Console.WriteLine("当前时间为:{0:HH:mm:ss.fff}", DateTime.Now);
         }, token);
         await Task.Delay(500);
     
         Console.WriteLine("当前时间为:{0:HH:mm:ss.fff}", DateTime.Now);
         cts.Cancel(true);
         //把此处的Cancel(true)改为Cancel()或Cancel(false)会执行到所有注册回调结束
         await task;
         Console.WriteLine($"IsCancellationRequested after cancel: {token.IsCancellationRequested}");
     }
     catch (OperationCanceledException)
     {
         Console.WriteLine("Operation was canceled.");
     }
     catch (AggregateException ae)
     {
         Console.WriteLine("catch AggregateException.");
         foreach (var e in ae.InnerExceptions)
         {
             if (e is OperationCanceledException)
             {
                 Console.WriteLine("Task was canceled.");
             }
             else
             {
                 Console.WriteLine($"Task failed: {e.Message}");
             }
         }
     }
     catch(Exception ex)
     {
         Console.WriteLine($"Unexpected exception: {ex.Message}");
     }
     finally
     {
         cts.Dispose();
     }
     cts.Dispose();
 }

output

IsCancellationRequested: False
CanBeCanceled: True
Doing work... 1
Doing work... 2
Doing work... 3
Doing work... 4
Doing work... 5
Doing work... 6
Doing work... 7
Doing work... 8
Doing work... 9
Doing work... 10
当前时间为:02:00:15.379
当前时间为:02:00:15.727
最后一个注册,被取消后首个调用的回调函数.
Unexpected exception: 空引用了

使用Cancel()或cts.Cancel(false) 后 output

IsCancellationRequested: False
CanBeCanceled: True
Doing work... 1
Doing work... 2
Doing work... 3
Doing work... 4
Doing work... 5
Doing work... 6
Doing work... 7
Doing work... 8
Doing work... 9
Doing work... 10
当前时间为:02:02:34.876
当前时间为:02:02:35.244
最后一个注册,被取消后首个调用的回调函数.
第二个注册的回调函数.
Cancellation requested,首个注册的回调函数.
catch AggregateException.
Task failed: 空引用了
Task failed: Stack overflow in cancellation callback.
Task failed: Specified argument was out of the range of valid values. (Parameter 'Amount of shapes must be positive.')

可以看到返回的异常全部以AggregateException整合后抛出,且只要执行Cancel,Register注册的回调函数一定会调用,不管任务有没有执行完

同样关于定时取消也可以在构造CancellationTokenSource时作为参数传入TimeSpan

CancellationTokenSource cts = new CancellationTokenSource(1000);  

_ = Task.Run(()=>{...},cts.Token);

同时取消令牌只能取消一次,也就说只能做一次状态转换,已经取消的状态不能转换回来

CancelAfter()

有两个重载

public void CancelAfter(int millisecondsDelay);

public void CancelAfter(TimeSpan delay);

注意事项: 多次Cancel 以最后一次为准

举例子

 public static async Task CTSCancelAfterTest()
 {
     var cts = new CancellationTokenSource();
     var token = cts.Token;

     token.Register(() => {
         Console.WriteLine("已经取消;当前时间{0:HH:mm:ss.fff}",DateTime.Now);
     });
     try
     {
         var task = Task.Run(() =>
         {
             for (int i = 0; i < 30; i++)
             {
                 token.ThrowIfCancellationRequested();
                 Console.WriteLine($"Doing work... {i + 1}");
                 Thread.Sleep(200);
             }
             Console.WriteLine("任务结束,当前时间为:{0:HH:mm:ss.fff}", DateTime.Now);
         }, token);
         await Task.Delay(500);
         Console.WriteLine("两秒钟后取消,当前时间为:{0:HH:mm:ss.fff}", DateTime.Now);
         cts.CancelAfter(2000); // 2秒后取消
         Console.WriteLine("三秒钟后取消,当前时间为:{0:HH:mm:ss.fff}", DateTime.Now);
         cts.CancelAfter(3000); // 1秒后取消,以最后一次为准
         //
         await task;
     }
     catch (OperationCanceledException)
     {
         Console.WriteLine("Operation was canceled.");
     }
     catch (AggregateException ae)
     {
         foreach (var e in ae.InnerExceptions)
         {
             if (e is OperationCanceledException)
             {
                 Console.WriteLine("Task was canceled.");
             }
             else
             {
                 Console.WriteLine($"Task failed: {e.Message}");
             }
         }
     }
     finally
     {
         cts.Dispose();
     }
 }

在我的电脑上输出

Doing work... 1
Doing work... 2
Doing work... 3
两秒钟后取消,当前时间为:23:29:20.553
三秒钟后取消,当前时间为:23:29:20.563
Doing work... 4
Doing work... 5
Doing work... 6
Doing work... 7
Doing work... 8
Doing work... 9
Doing work... 10
Doing work... 11
Doing work... 12
Doing work... 13
Doing work... 14
Doing work... 15
Doing work... 16
Doing work... 17
已经取消;当前时间23:29:23.566
Operation was canceled.

可以看到任务实际是在最后一个三秒钟内取消的

关联取消,CreateLinkedTokenSource

四个重载

public static System.Threading.CancellationTokenSource CreateLinkedTokenSource (scoped ReadOnlySpan<System.Threading.CancellationToken> tokens);

public static System.Threading.CancellationTokenSource CreateLinkedTokenSource (System.Threading.CancellationToken token);

public static System.Threading.CancellationTokenSource CreateLinkedTokenSource (params System.Threading.CancellationToken[] tokens);

public static System.Threading.CancellationTokenSource CreateLinkedTokenSource (System.Threading.CancellationToken token1, System.Threading.CancellationToken token2);

MSDN有个例子

首先CT-Cancellationtoken只是个取消信号接受器,信号发射器是CTS,当我们从外部获取一个或多个token时我自己在函数内部也想取消或者一个取消多个token一起取消,可以用此方法把token都关联起来主动管理

注意调用时token状态是取消了,创建的CTS也是取消状态,第三个数值重载中有一个token是取消状态创建的CTS也是取消状态