TaskCanceledException using Task <string>

An overview of what I am doing: In a loop, I start a new one Task<string>

and add it to List<Task<string>>

. The problem is that after the row is returned, the task is thrown System.Threading.Tasks.TaskCanceledException

and I don't know why. Below is a summary version of what I am doing.

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>>();

      

My loop looks like this:

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

      

When I go through the code it gets caught in return "my xml data";

, but an exception is thrown.

What I am trying to avoid with ContinueWith

is when I loop over each task and get results, it does not throw the same AggregateException

that it selected with WaitAll

.

Here is a work console app that throws ... I know the problem is related to ContinueWith

, but why?

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


source to share


3 answers


You are doing the second task.
.ContinueWith ((t)).

To run the correct one, you need to refactor your code.
Split the line like this:

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

      



You can reorganize tasks like this: (for error handling)

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

      

+2


source


As consultant Avram points out, you get an exception because your list does not contain tasks that are performed by a method GenerateXml()

, but those that are continuation of tasks that perform that method.

Since these tasks only run when they GenerateXml()

throw an exception, if any call GenerateXml()

succeeds, then at least one of these continuation tasks will not run. Instead, it completes by canceling (i.e., when its antecedent succeeds) and so the call WaitAll()

sees that it cancels and throws a rolling exception.

IMHO, the best way to solve this problem is to stick with the async

/ pattern await

. That is, instead of using it ContinueWith()

directly, write the code so that it is readable and expressive. In this case, I would write a wrapper method async

to invoke the method GenerateXml()

, catching any exception thrown, and returning a value ""

in that case.



Here's a modified version of your MCVE to show what I mean:

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";
    }
}

      

IMHO this is much more in line with the new idioms async

in C #, much less prone to failure, and much easier to understand what exactly is going on (i.e. avoiding ContinueWith()

altogether, you don't even have the opportunity to get confused about what tasks (tasks) are waiting for like you obviously done in their source code).

+3


source


Since you are using .ContinueWith

as a way to recover from an exception, you can simply add a statement 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());

      

However, if I were you, I would hide this logic in a method GenerateXml

. As long as you consider a valid default (empty string here) it should be fine.

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

      

+2


source







All Articles