异常时获取任务状态
Getting Task's state on exception
我正在构建一个需要执行许多并发任务的应用程序。
这些任务包含对不同异步方法的多次调用(主要是,它使用 HttpClient 查询一些 REST API),中间有一些处理,我真的不想在任务本身中捕获异常。相反,我更愿意在等待他们使用 WhenAny/WhenAll 方法时这样做。
当出现异常时,我还需要捕获该任务的一些源数据以供将来分析(例如,将其写入日志)。
有一个 Task.AsyncState 属性,它似乎是此数据的完美容器。所以,我在创建任务时传递一个 TaskState 对象,如下所示:
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");
}
}
}
上面代码的问题是 Unwrap() 创建了一个没有 AsyncState 值的任务代理,它总是空的,所以当遇到异常时,我无法获取原始任务的状态。如果我删除委托中的 Unwrap() 和 async/await 修饰符,WaitAny() 方法会在任务实际完成之前完成。
如果我使用 Task.Run(() => DoJobAsync(state))
而不是 TaskFactory.StartNew(),任务会以正确的顺序完成,但这样我就无法在创建任务时设置 AsyncState。
当然,我可以使用 Dictionary<Task, TaskState>
来保留任务参数以防失败,但对我来说似乎有点脏,而且,与 AsyncState 属性 的用法相比,它如果有大量并发任务,将需要时间在此集合中搜索匹配项。
在这种情况下,有人可以提出更优雅的解决方案吗?是否有另一种方法可以在异常时获取任务的状态?
一个适合您的优雅解决方案是:
var task = DoJobAsync(state);
这样传递你的数据,执行顺序是正确的
出于记录目的,在操作失败时包含有关操作正在执行的信息的正确位置是您抛出的异常。不要强迫调用者不仅要跟踪任务,还要跟踪有关该任务的大量信息,以便他们可以适当地处理错误。
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");
}
如果在您的真实代码中抛出的异常没有被显式抛出,而是由您无法控制的代码抛出,那么您可能需要捕获这些异常并将它们包装在您自己的新异常中(这意味着它需要接受内部异常并将其传递给基本构造函数)。
还有几点要注意,不要使用StartNew
或Run
来运行一个[=22]的操作=]已经是异步的。只是 运行 方法。您不需要 启动 一个已经异步操作的新线程池线程(除非它一开始就写得不正确)。如果您想在任务失败时 运行 编写一些代码 使用 try catch,而不是尝试执行您正在做的事情。它增加了很多复杂性,并且有更多的错误空间。
值得注意的是,由于我重构错误处理代码的方式,初始输入数据仍在范围内,因此甚至不需要将其包含在异常中,但我将其保留在那里是因为错误处理代码可能位于调用堆栈的更上层,或者要打印的信息不仅仅是提供给函数的输入。但如果这些都不成立,您可以只使用 catch
块中的输入数据,因为它仍在范围内。
我正在构建一个需要执行许多并发任务的应用程序。 这些任务包含对不同异步方法的多次调用(主要是,它使用 HttpClient 查询一些 REST API),中间有一些处理,我真的不想在任务本身中捕获异常。相反,我更愿意在等待他们使用 WhenAny/WhenAll 方法时这样做。 当出现异常时,我还需要捕获该任务的一些源数据以供将来分析(例如,将其写入日志)。
有一个 Task.AsyncState 属性,它似乎是此数据的完美容器。所以,我在创建任务时传递一个 TaskState 对象,如下所示:
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");
}
}
}
上面代码的问题是 Unwrap() 创建了一个没有 AsyncState 值的任务代理,它总是空的,所以当遇到异常时,我无法获取原始任务的状态。如果我删除委托中的 Unwrap() 和 async/await 修饰符,WaitAny() 方法会在任务实际完成之前完成。
如果我使用 Task.Run(() => DoJobAsync(state))
而不是 TaskFactory.StartNew(),任务会以正确的顺序完成,但这样我就无法在创建任务时设置 AsyncState。
当然,我可以使用 Dictionary<Task, TaskState>
来保留任务参数以防失败,但对我来说似乎有点脏,而且,与 AsyncState 属性 的用法相比,它如果有大量并发任务,将需要时间在此集合中搜索匹配项。
在这种情况下,有人可以提出更优雅的解决方案吗?是否有另一种方法可以在异常时获取任务的状态?
一个适合您的优雅解决方案是:
var task = DoJobAsync(state);
这样传递你的数据,执行顺序是正确的
出于记录目的,在操作失败时包含有关操作正在执行的信息的正确位置是您抛出的异常。不要强迫调用者不仅要跟踪任务,还要跟踪有关该任务的大量信息,以便他们可以适当地处理错误。
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");
}
如果在您的真实代码中抛出的异常没有被显式抛出,而是由您无法控制的代码抛出,那么您可能需要捕获这些异常并将它们包装在您自己的新异常中(这意味着它需要接受内部异常并将其传递给基本构造函数)。
还有几点要注意,不要使用StartNew
或Run
来运行一个[=22]的操作=]已经是异步的。只是 运行 方法。您不需要 启动 一个已经异步操作的新线程池线程(除非它一开始就写得不正确)。如果您想在任务失败时 运行 编写一些代码 使用 try catch,而不是尝试执行您正在做的事情。它增加了很多复杂性,并且有更多的错误空间。
值得注意的是,由于我重构错误处理代码的方式,初始输入数据仍在范围内,因此甚至不需要将其包含在异常中,但我将其保留在那里是因为错误处理代码可能位于调用堆栈的更上层,或者要打印的信息不仅仅是提供给函数的输入。但如果这些都不成立,您可以只使用 catch
块中的输入数据,因为它仍在范围内。