在Task中使用CancellationToken作为超时.运行不起作用

本文关键字:超时 运行 不起作用 CancellationToken Task | 更新日期: 2023-09-27 17:53:21

好的,我的问题很简单。为什么这段代码没有抛出TaskCancelledException ?

static void Main()
{
    var v = Task.Run(() =>
    {
        Thread.Sleep(1000);
        return 10;
    }, new CancellationTokenSource(500).Token).Result;
    Console.WriteLine(v); // this outputs 10 - instead of throwing error.
    Console.Read();
}

但是这个可以

static void Main()
{
    var v = Task.Run(() =>
    {
        Thread.Sleep(1000);
        return 10;
    }, new CancellationToken(true).Token).Result;
    Console.WriteLine(v); // this one throws
    Console.Read();
}

在Task中使用CancellationToken作为超时.运行不起作用

托管线程取消:

取消是协作的,不会强加给侦听器。侦听器确定如何优雅地终止以响应取消请求。

您没有在Task.Run方法中编写任何代码来访问CancellationToken并实现取消-因此您有效地忽略了取消请求并运行到完成

取消正在运行的任务和计划运行的任务是不同的。

在调用Task之后。

当你使用Task.Run(…(CancellationToken)类重载的取消支持,在任务即将运行时检查取消令牌。如果此时取消令牌将IsCancellationRequested设置为true,则抛出TaskCanceledException类型的异常。

如果任务已经在运行,则任务的责任是调用ThrowIfCancellationRequested方法,或者只是抛出OperationCanceledException。

根据MSDN,这只是一个方便的方法,用于以下内容:

如果(token.IsCancellationRequested)throw new OperationCanceledException(token);

注意这两种情况下使用的不同类型的异常:

catch (TaskCanceledException ex)
{
    // Task was canceled before running.
}
catch (OperationCanceledException ex)
{
    // Task was canceled while running.
}

还要注意,TaskCanceledException派生自OperationCanceledException,因此对于OperationCanceledException类型,您只能有一个catch子句:

catch (OperationCanceledException ex)
{
    if (ex is TaskCanceledException)
        // Task was canceled before running.
    // Task was canceled while running.
}

在代码的第一个变体中,您没有做任何事情来管理取消令牌。

例如,您没有检查token.IsCancellationRequested是否返回true(然后抛出异常)或从CancellationToken对象调用ThrowIfCancellationRequested()

此外,您使用的Task.Run过载检查令牌是否已经取消,当任务即将启动时,您的代码声明令牌将在500毫秒后报告取消。因此,您的代码只是忽略了取消请求,这就是任务运行到完成的原因。

你应该这样做:

void Main()
{
    var ct = new CancellationTokenSource(500).Token;
     var v = 
     Task.Run(() =>
    {
        Thread.Sleep(1000);
        ct.ThrowIfCancellationRequested();
        return 10;
    }, ct).Result;
    
    Console.WriteLine(v); //now a TaskCanceledException is thrown.
    Console.Read();
}

或this,不传递令牌,正如其他人已经注意到的:

void Main()
{
    var ct = new CancellationTokenSource(500).Token;
    ct.ThrowIfCancellationRequested();
    var v = 
     Task.Run(() =>
    {
        Thread.Sleep(1000);
        return 10;
    }).Result;
    
    Console.WriteLine(v); //now a TaskCanceledException is thrown.
    Console.Read();
}

代码的第二个变体可以工作,因为您已经初始化了一个令牌,并将Canceled状态设置为true。确实,如这里所述:

If canceled is true, both CanBeCanceled and IsCancellationRequested will be true

已经请求取消,然后异常TaskCanceledException将立即抛出,而不实际启动任务。

另一个使用Task的实现。用token代替Thread.Sleep延迟。

 static void Main(string[] args)
    {
        var task = GetValueWithTimeout(1000);
        Console.WriteLine(task.Result);
        Console.ReadLine();
    }
    static async Task<int> GetValueWithTimeout(int milliseconds)
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;
        cts.CancelAfter(milliseconds);
        token.ThrowIfCancellationRequested();
        var workerTask = Task.Run(async () =>
        {
            await Task.Delay(3500, token);
            return 10;
        }, token);
        try
        {
            return await workerTask;
        }
        catch (OperationCanceledException )
        {
            return 0;
        }
    }