TaskCanceledException使用Task <string>

时间:2015-06-13 20:35:11

标签: c#

我正在做的事情的概述:在一个循环中,我开始一个新的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避免的是当我循环每个任务并获得结果时,它不会抛出与AggregateException一样的WaitAll

这是一个工作的控制台应用程序抛出...我知道问题出在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";
    }
}

3 个答案:

答案 0 :(得分:3)

正如回答者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(),你甚至没有机会对正在等待哪些任务感到困惑,正如你在原始代码中所做的那样。)

答案 1 :(得分:2)

由于您使用.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();

答案 2 :(得分:2)

您正在执行第二项任务 .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; }));