TaskCanceledException using Task<string>

本文关键字:string gt using lt TaskCanceledException Task | 更新日期: 2023-09-27 17:56:08

我正在做的事情的概述:在一个循环中,我正在启动一个新的Task<string>并将其添加到List<Task<string>>中。 问题是,返回字符串后,任务抛出System.Threading.Tasks.TaskCanceledException,我不知道为什么。 以下是我正在做的事情的修剪版本

public async Task<string> GenerateXml(object item)
{
    using (var dbContext = new DatabaseContext())
    {
        //...do some EF dbContext async calls here
        //...generate the xml string and return it
        return "my xml data";
    }
}
var tasks = new List<Task<string>>();

我的循环如下所示:

foreach (var item in items)
{
    tasks.Add(Task.Run(() => GenerateXml(item).ContinueWith((t) => { return ""; }, TaskContinuationOptions.OnlyOnFaulted)));
    //also tried: 
    tasks.Add(Task.Run(async () => await GenerateXml(item).ContinueWith((t) => { return ""; }, TaskContinuationOptions.OnlyOnFaulted)));
    //both generate the same exception
    //after looking at my code, I was using the ContinueWith on the GenerateXml method call, which should still work, right?
    //I moved the continue with to the `Task.Run` and still get the exception.
}
Task.WaitAll(tasks.ToArray()); //this throws the AggregateException which contains the TaskCanceledException

当我单步执行代码时,它命中return "my xml data";但抛出异常。

我试图避免ContinueWith是当我循环每个任务并获得结果时,它不会抛出与WaitAll抛出的相同AggregateException

这是一个工作控制台应用程序,它抛出... 我知道问题出在ContinueWith,但为什么呢?

class Program
{
    static void Main(string[] args)
    {
        var program = new Program();
        var tasks = new List<Task<string>>();
        tasks.Add(Task.Run(() => program.GenerateXml().ContinueWith((t) => { return ""; }, TaskContinuationOptions.OnlyOnFaulted)));
        Task.WaitAll(tasks.ToArray()); //this throws the AggregateException
        foreach (var task in tasks)
        {
            Console.WriteLine(task.Result);
        }
        Console.WriteLine("finished");
        Console.ReadKey();
    }
    public async Task<string> GenerateXml()
    {
        System.Threading.Thread.Sleep(3000);
        return "my xml data";
    }
}

TaskCanceledException using Task<string>

正如回答者 Avram 所暗示的那样,您会收到异常,因为您的列表包含的不是运行 GenerateXml() 方法的任务,而是那些运行该方法的任务的延续。

由于这些任务仅在GenerateXml()引发异常时运行,因此如果GenerateXml()的任何调用成功,则至少一个继续任务将不会运行。相反,它通过被取消(即当其先前任务成功完成时)来完成,因此对 WaitAll() 的调用会看到该取消并引发聚合异常。

恕我直言,解决这个问题的最佳方法是坚持使用 async/await 模式。 即,与其直接使用ContinueWith(),不如编写代码,使其具有可读性和表达性。在这种情况下,我将编写一个包装器async方法来调用 GenerateXml() 方法,捕获发生的任何异常,并在该情况下返回 "" 值。

这是您的MCVE的修改版本,显示了我的意思:

class Program
{
    static void Main(string[] args)
    {
        var tasks = new List<Task<string>>();
        tasks.Add(SafeGenerateXml());
        Task.WaitAll(tasks.ToArray());
        foreach (var task in tasks)
        {
            Console.WriteLine(task.Result);
        }
        Console.WriteLine("finished");
        Console.ReadKey();
    }
    static async Task<string> SafeGenerateXml()
    {
        try
        {
            return await GenerateXml();
        }
        catch (Exception)
        {
            return "";
        }
    }
    static async Task<string> GenerateXml()
    {
        await Task.Delay(3000);
        return "my xml data";
    }
}

恕我直言,这更符合 C# 中的新async习语,更不容易失败,并且更容易理解到底发生了什么(即通过完全避免ContinueWith(),你甚至没有机会混淆哪些任务正在等待,就像你在原始代码中显然所做的那样)。

您正在运行第二个任务。
.继续((t) 任务。

要运行正确的代码,您需要重构代码。
像这样拆分行:

    Task<string> t1 = Task.Run(() => program.GenerateXml());
    t1.ContinueWith((t) => { return ""; }, TaskContinuationOptions.OnlyOnFaulted);
    tasks.Add(t1);

您可以像这样重构任务:(用于错误处理)

tasks.Add(program.GenerateXml().ContinueWith(t => {return t.IsFaulted? "": t.Result; }));

由于您使用.ContinueWith作为从异常中恢复的一种方式,因此只需添加 try {} catch 语句即可。

   var tasks = new List<Task<string>>();
   foreach (var item in items)
   {
         var closure = item;
         var task =
             Task.Factory.StartNew(
                  async () =>
                   {
                      try
                      {
                          return await GenerateXml(closure);
                      }
                      catch (Exception exception)
                      {
                              //log
                              return "";
                      }
                  }).Unwrap();
         tasks.Add(task);
     }
     Task.WaitAll(tasks.ToArray());

但是,如果我是你,我会将此逻辑隐藏在GenerateXml方法中。只要您将默认值(此处为空字符串)视为有效值,就应该没问题。

var tasks = items.Select(item => Task.Run(() => GenerateXml(item))).ToList();