【问题标题】:Is the below code captures the exceptions from original, continuation and child tasks in TPL?下面的代码是否捕获了 TPL 中原始任务、延续任务和子任务的异常?
【发布时间】:2020-07-21 04:08:51
【问题描述】:

我正在使用 TPL 和 async/await 在 webclient 之上为我的应用程序构建异步 API。很少有地方(通常是我需要运行一堆异步任务并最终等待所有这些任务的地方),我遵循代码 sn-p。我只是想确保我得到它是正确的,即使使用 TPL 和 async/await 编写异步代码相对容易 调试/故障排除仍然具有挑战性(交互式调试和客户站点的故障排除问题) - 所以想把它做好。

我的目标: 能够捕获原始任务、后续任务以及子任务产生的异常,以便我处理(如果需要)。我不希望任何例外都被遗忘。

我使用的基本原则: 1. .net 框架确保将异常附加到任务 2. try/catch 块可以应用于 async/await 以提供同步代码的错觉/可读性(参考:http://channel9.msdn.com/Events/TechDays/Techdays-2014-the-Netherlands/Async-programming-deep-divehttp://blogs.msdn.com/b/ericlippert/archive/2010/11/19/asynchrony-in-c-5-part-seven-exceptions.aspxhttp://msdn.microsoft.com/en-us/library/dd537614.aspx 等)

问题:我希望获得批准,即我希望的目标(我可以从原始任务、延续任务和子任务中捕获异常)已经实现,并且我可以对示例:

例如,是否会出现这样一种情况,其中一个组合任务(例如,解包的代理任务)根本不会被激活(waitforactivation 状态),因此 waitall 可能只是等待任务启动?我的理解是,这些情况永远不会发生,因为延续任务总是执行,并返回一个已由代理使用 wnwrap 跟踪的任务。只要我在所有层和 api 中遵循类似的模式,该模式就应该捕获链式任务中的所有聚合异常。

注意: 本质上是在寻找建议,例如,如果原始任务状态未完成,则避免在继续任务中创建虚拟任务,或者使用附加到父级以便我只能等待父级等. 查看所有可能性,以便我可以选择最佳选项,因为这种模式在我的应用程序中严重依赖于错误处理。

static void SyncAPIMethod(string[] args)
        {
            try
            {
                List<Task> composedTasks = new List<Task>();
                //the underlying async method follow the same pattern
                //either they chain the async tasks or, uses async/await 
                //wherever possible as its easy to read and write the code
                var task = FooAsync();
                composedTasks.Add(task);
                var taskContinuation = task.ContinueWith(t =>
                    {
                        //Intentionally not using TaskContinuationOptions, so that the 
                        //continuation task always runs - so that i can capture exception
                        //in case something is wrong in the continuation
                        List<Task> childTasks = new List<Task>();
                        if (t.Status == TaskStatus.RanToCompletion)
                        {

                            for (int i = 1; i <= 5; i++)
                            {
                                var childTask = FooAsync();
                                childTasks.Add(childTask);
                            }

                        }
                        //in case of faulted, it just returns dummy task whose status is set to 
                        //'RanToCompletion'
                        Task wa = Task.WhenAll(childTasks);
                        return wa;
                    });
                composedTasks.Add(taskContinuation);
                //the unwrapped task should capture the 'aggregated' exception from childtasks
                var unwrappedProxyTask = taskContinuation.Unwrap();
                composedTasks.Add(unwrappedProxyTask);
                //waiting on all tasks, so the exception will be thrown if any of the tasks fail
                Task.WaitAll(composedTasks.ToArray());
            }
            catch (AggregateException ag)
            {
                foreach(Exception ex in ag.Flatten().InnerExceptions)
                {
                    Console.WriteLine(ex);
                    //handle it
                }
            }
        }

【问题讨论】:

  • 这个问题似乎跑题了,因为它是一个代码审查问题,应该在 codereview.stackexchange.com 上
  • @Eugene,我知道我最有可能使用的模式 - 但目的是确保并使其变得更好。例如,在连续块中,我总是用任务创建虚拟任务,以确保我可以捕获未包装的任务错误。使用 unwrap 对嵌套任务也是如此。只是想确保使用正确的扩展(例如,将延续和子任务附加到原始任务并等待它会更好吗?)
  • IMO,使用async/await,这段代码本可以更简单、更优雅。我不明白您为什么坚持使用ContinueWithUnwrap,以及为什么将内部和外部(未包装)任务都添加到composedTasks
  • @Noseratio,无论我在哪里使用/能够在异步 API/方法中使用异步/等待,我都只是将它们包装在 try/catch 中(是的,它更好更优雅)。但请注意,这不是“异步”方法,而是“同步”调用——有些 api 仍然需要 syn api 支持。因此,为此我不得不使用 continue/unwrap 来组合所有任务并最终等待它们。
  • @Noseration,现在我明白了:)。不过我有一个问题,请看下文。问候。

标签: c# .net asynchronous task-parallel-library async-await


【解决方案1】:

来自cmets:

IMO,这段代码本可以更简单、更优雅 使用异步/等待。我不明白你坚持的原因 ContinueWith 和 Unwrap,以及为什么同时添加内部和外部 (解包)任务到composedTasks。

我的意思是像下面这样。我认为它与原始代码的作用相同,但没有composedTasksContinueWithUnwrap 形式的不必要冗余。如果你使用async/await,你几乎不需要这些。

static void Main(string[] args)
{
    Func<Task> doAsync = async () =>
    {
        await FooAsync().ConfigureAwait(false);

        List<Task> childTasks = new List<Task>();
        for (int i = 1; i <= 5; i++)
        {
            var childTask = FooAsync();
            childTasks.Add(childTask);
        }

        await Task.WhenAll(childTasks);
    };

    try
    {
        doAsync().Wait();
    }
    catch (AggregateException ag)
    {
        foreach (Exception ex in ag.Flatten().InnerExceptions)
        {
            Console.WriteLine(ex);
            //handle it
        }
    }
}

static async Task FooAsync()
{
    // simulate some CPU-bound work
    Thread.Sleep(1000); 
    // we could have avoided blocking like this:        
    // await Task.Run(() => Thread.Sleep(1000)).ConfigureAwait(false);

    // introduce asynchrony
    // FooAsync returns an incomplete Task to the caller here
    await Task.Delay(1000).ConfigureAwait(false);
}

更新以解决评论:

在某些用例中,我在“创建子任务”后继续 调用更多“独立”任务。

基本上,任何异步任务工作流都存在三种常见场景:顺序组合、并行组合或这两者的任意组合(混合组合):

  • 顺序组合:

    await task1;
    await task2;
    await task3;
    
  • 平行组合:

    await Task.WhenAll(task1, task2, task3);
    
    // or
    
    await Task.WhenAny(task1, task2, task3);
    
  • 混合成分:

    var func4 = new Func<Task>(async () => { await task2; await task3; });
    await Task.WhenAll(task1, func4());
    

如果上述任何任务执行 CPU 密集型工作,您可以为此使用 Task.Run,例如:

    var task1 = Task.Run(() => CalcPi(numOfPiDigits));

其中CalcPi 是进行实际计算的同步方法。

【讨论】:

  • +1 哦,很酷 - 这好多了。不过我有一个问题——在我的版本中,代码保证调用的线程会做一些 CPU 工作,然后等待。但是使用 lamda 中的 async/await 版本,看起来线程最有可能立即等待。
  • @Dreamer,和你的版本一样,这完全取决于FooAsync 里面的内容。调用doAsync 的线程将被阻塞,直到在FooAsync 的某个地方引入异步。我已经更新了答案以说明,请注意 Thread.Sleep(1000)FooAsync 内,它是您可能在那里进行的任何 CPU 绑定工作的占位符。
  • 但使用此“doAsync”只会在“await”语句中返回生成的任务。如果异步任务从 await 旁边的语句完成,它就会恢复,并开始编写子任务。但是,在我的问题版本中,一旦 fooasync 返回,我仍然编写并执行更多 CPU 绑定工作 - 而在这里,它仅在 doasync 中的第一个等待完成后阻塞并执行更多 CPU 工作。
  • @Dreamer,你指的是什么 CPU 工作?对于composedTasks,我认为您没有在代码中与fooasync 进行并行 操作。这是因为你在ContinueWith 中所做的任何事情都将在fooasync 完成 之后执行。从本质上讲,添加到 composedTasks 的 3 个任务中只有 1 个是“热”的,这与 childTask 不同。
  • 哦,从示例中看确实如此 - 但在某些用例中,我会在“创建子任务”后继续调用更多“独立”任务。但是,你是对的 - 我应该更新这个问题。让我考虑一下,明天更新(睡觉时间)。如果我能在所有其他情况下使用你的模式,我明天会继续接受它作为答案。谢谢。
猜你喜欢
  • 2023-03-29
  • 1970-01-01
  • 2018-09-06
  • 2013-01-07
  • 2013-08-17
  • 2014-03-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多