【问题标题】:Getting Task's state on exception获取异常时任务的状态
【发布时间】:2018-12-09 14:06:24
【问题描述】:

我正在构建一个需要执行许多并发任务的应用程序。 这些任务包含对不同异步方法的多次调用(主要是使用 HttpClient 查询一些 REST API),中间有一些处理,我真的不想在任务本身中捕获异常。相反,我更愿意在使用 WhenAny/WhenAll 方法等待他们时这样做。 遇到异常时,我还需要捕获该任务的一些源数据以供将来分析(例如,将其写入日志)。

有一个 Task.AsyncState 属性,它似乎是该数据的完美容器。因此,我在 Task 的创建过程中传递了一个 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(() =&gt; DoJobAsync(state)) 而不是 TaskFactory.StartNew(),任务会以正确的顺序完成,但这样我就无法在任务创建时设置 AsyncState。

当然,我可以使用Dictionary&lt;Task, TaskState&gt; 来保存任务参数以防失败,但这对我来说似乎有点脏,并且与使用 AsyncState 属性相比,搜索匹配项需要时间在并发任务量大的情况下在这个集合中。

在这种情况下,有人可以提出更优雅的解决方案吗?有没有其他方法可以在异常时获取任务的状态?

【问题讨论】:

    标签: c# .net multithreading concurrency task-parallel-library


    【解决方案1】:

    为了记录操作失败的目的,包含有关操作正在做什么的信息的正确位置是在您抛出的异常中。不要强迫调用者不仅要跟踪任务,还要跟踪有关该任务的大量信息,以便他们能够适当地处理错误。

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

    如果在您的真实代码中抛出的异常没有被显式抛出,而是由您无法控制的代码抛出,那么您可能需要捕获这些异常并将它们包装在您自己的新异常中(意思是它需要接受一个内部异常并将其传递给基本构造函数)。

    还有一些需要注意的事项,不要使用任一StartNewRun 来运行已经是异步的操作。只需运行该方法。您不需要启动一个已经异步操作的新线程池线程(除非它一开始就写得不正确)。如果您想在任务失败时运行一些代码使用 try catch,而不是尝试做您正在做的事情。它增加了很多复杂性,并且有更多的错误空间。

    值得注意的是,由于我如何重构错误处理代码,初始输入数据仍在范围内,因此甚至不需要将其包含在异常中,但我将其保留在那里,因为它是可能的错误处理代码将位于调用堆栈的更上方,或者要打印的信息不仅仅是提供给函数的输入。但如果这些都不成立,您可以只使用 catch 块中的输入数据,因为它仍在范围内。

    【讨论】:

    • 感谢您提供如此全面的答案。现在我真的看到最好在任务方法本身中捕获异常。
    【解决方案2】:

    一个优雅的解决方案是:

    var task = DoJobAsync(state);
    

    这样你传递你的数据,执行顺序是正确的。

    【讨论】:

    • 感谢您指出在这种情况下使用单独的任务对象的冗余,现在我的代码看起来好多了。但是传递状态并不是真正的问题,问题是在异常时检索它。
    • 但您也可以在异常时检索它。 :-)
    猜你喜欢
    • 1970-01-01
    • 2013-11-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-09
    • 1970-01-01
    相关资源
    最近更新 更多