Reputation: 372
I'm building an application that needs to execute many concurrent tasks. Those tasks contains multiple calls to different async methods (mostly, it's querying some REST APIs using HttpClient) with some processing in between, and I really don't want to catch exceptions inside the task themselves. Instead, I would prefer to do it while waiting for them using WhenAny/WhenAll methods. When getting an exception, I need to also capture some source data of that task for future analysis (say, to write it in log).
There's an Task.AsyncState property, which seems a perfect container for this data. So, I'm passing a TaskState object on Task's creation like this:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ConsoleApp5
{
class Program
{
static void Main(string[] args)
{
new Program().RunTasksAsync();
Console.ReadLine();
}
class TaskState
{
public int MyValue { get; }
public TaskState(int myValue)
{
MyValue = myValue;
}
}
private async Task<int> AsyncMethod1(int value)
{
await Task.Delay(500);
return value + 1;
}
private async Task<int> AsyncMethod2(int value)
{
await Task.Delay(500);
if (value % 3 == 0)
throw new Exception("Expected exception");
else
return value * 2;
}
private async Task DoJobAsync(object state)
{
var taskState = state as TaskState;
var i1 = await AsyncMethod1(taskState.MyValue);
var i2 = await AsyncMethod2(i1);
Console.WriteLine($"The result is {i2}");
}
private async void RunTasksAsync()
{
const int tasksCount = 10;
var tasks = new List<Task>(tasksCount);
var taskFactory = new TaskFactory();
for (var i = 0; i < tasksCount; i++)
{
var state = new TaskState(i);
var task = taskFactory.StartNew(async state => await DoJobAsync(state), state).Unwrap();
tasks.Add(task);
}
while (tasks.Count > 0) {
var task = await Task.WhenAny(tasks);
tasks.Remove(task);
var state = task.AsyncState as TaskState;
if (task.Status == TaskStatus.Faulted)
Console.WriteLine($"Caught an exception while executing task, task data: {state?.MyValue}");
}
Console.WriteLine("All tasks finished");
}
}
}
The problem with the code above is Unwrap() creates a Task's proxy which doesn't AsyncState value, it's always null, so when it comes to exception, I can't get the original task's state. If I remove the Unwrap() and async/await modifiers in the delegate, the WaitAny() method completes before the task is actually finished.
If I use Task.Run(() => DoJobAsync(state))
instead of TaskFactory.StartNew(), tasks finish in correct order, but this way I can't set the AsyncState on tasks' creation.
Of course, I can use a Dictionary<Task, TaskState>
to keep tasks parameters in case of failure, but is seems a bit dirty to me, and, comparing to the usage of AsyncState property, it will take time to search for the match in this collection in case of large amount of concurrent tasks.
Can someone suggest a more elegant solution in this case? Is there another way to get Task's state on exception?
Upvotes: 1
Views: 1079
Reputation: 203830
The proper place to include information about what the operation was doing, for the purposes of logging, when it failed is in the exception that you throw. Don't force the caller to have to keep track of not only the task, but a bunch of information about that task so that they can handle the errors appropriately.
public class ExpectedException : Exception
{
public int Value { get; }
public ExpectedException(int value, string message) : base(message)
{
Value = value;
}
}
private async Task<int> AsyncMethod1(int value)
{
await Task.Delay(500);
return value + 1;
}
private async Task<int> AsyncMethod2(int value)
{
await Task.Delay(500);
if (value % 3 == 0)
throw new ExpectedException(value, "Expected exception");
else
return value * 2;
}
private async Task DoJobAsync(int value)
{
var i1 = await AsyncMethod1(value);
var i2 = await AsyncMethod2(i1);
Console.WriteLine($"The result is {i2}");
}
private async Task RunTasksAsync()
{
const int tasksCount = 10;
var tasks = Enumerable.Range(0, tasksCount)
.Select(async i =>
{
try
{
await DoJobAsync(i);
}
catch (ExpectedException exception)
{
Console.WriteLine($"Caught an exception while executing task, task data: {exception.Value}");
}
})
.ToList();
await Task.WhenAll(tasks);
Console.WriteLine("All tasks finished");
}
If the exception being thrown in your real code isn't being explicitly thrown, and is instead being thrown by code you don't control, then you may need to catch those exceptions and wrap them in your own new exception (meaning it'll need to accept an inner exception and pass it into the base constructor).
A few other things to note, don't use either StartNew
or Run
to run an operation that is already asynchronous. Just run the method. You don't need to start an already asynchronous operation new thread pool thread (unless it was written improperly to begin with). If you want to run some code when a task fails use a try catch, rather than trying to do what you were doing. It adds a lot of complexity and is a lot more room for errors.
It's worth noting that, due to how I refactored the error handling code, the initial input data is still in scope, so including it in the exception isn't even needed, but I'm leaving it in there because it's possible that the error handling code will be further up the call stack, or that the information to be printed is more than just the input provided to the function. But if neither of those holds, you can just use the input data in the catch
block, because it's still in scope.
Upvotes: 1
Reputation: 5042
An elegant solution for you is:
var task = DoJobAsync(state);
In this way you pass your data, and the execution order is correct.
Upvotes: 0