从复杂的任务链中捕获异常

Catching exceptions from complex chains of tasks

我有一个 .Net 核心应用程序,它基本上做这样的事情:

public async Task<IOBoundStuffResult> DoIOBoundStuff()
{
    // Do IO bound stuff ...
    
    return new IOBoundStuffResult
    {
        Id = getIdForThing()
    };
}

public async Task<OtherIOBoundStuffResult> DoOtherIOBoundStuff()
{
    // Do other IO bound stuff ...
    
    return new OtherIOBoundStuffResult
    {
        Uri = getUriForThing()
    };
}

public async Task<IOBoundTaskResult> DoIOBoundStuff()
{
    var ioBoundTask1 = doIOBoundStuff();
    var ioBoundTask2 = doOtherIOBoundStuff();
    
    return await Task.WhenAll(ioBoundTask1, ioBoundTask2)
        .ContinueWith((task) =>
        {
            var id = ioBoundTask1.Result.Id;
            var uri = ioBoundTask2.Result.Uri;
            
            doSomethingWithIdAndUri(id, uri);
            
            return new IOBoundTaskResult
            {
                Id = id,
                Uri = uri
            };
        });
}

public async Task<IActionResult> DoThing()
{
    try
    {
        var cpuBoundTask = Task.Run(() =>
        {
            doCPUBoundStuff();
        });
        
        var ioBoundTask = DoIOBoundStuff();
        
        // do stuff with ioBoundTask, cpuBoundTask
    }
    catch (System.Exception ex)
    {
        // Process System.Exception.AggregateException, other exceptions
    }
}

这里的问题是,如果其中一个任务中的某项抛出异常(特别是 doSomethingWithIdAndUri()),那么该异常不会被 try...catch 块捕获并导致崩溃。我已经尝试使用 TaskContinuationOptions.OnlyOnFaulted 创建延续任务来处理异常,但似乎所做的一切总是导致抛出 TaskCancelledException。如何捕获任务抛出的异常?

为了使任务的异常“冒泡”,必须等待它。如果在等待时抛出异常,则该异常将在当前上下文中重新抛出(尽管它有时可能被包装在另一种异常类型中,例如 AggregateException.

您需要将 var ioBoundTask = DoIOBoundStuff(); 更改为 var ioBoundTask = await DoIOBoundStuff();,或者通过等待任务并将等待包装在另一个 try-catch 块中来捕获其他地方的异常。

注意等待每个异步方法。避免使用 async void,因为您等不及了。相反,使用 async Task.

编辑添加: 您对 ContinueWith 的使用在异步上下文中似乎有点奇怪。而不是

return await Task.WhenAll(ioBoundTask1, ioBoundTask2)
        .ContinueWith((task) =>
        {
            var id = ioBoundTask1.Result.Id;
            var uri = ioBoundTask2.Result.Uri;
            
            doSomethingWithIdAndUri(id, uri);
            
            return new IOBoundTaskResult
            {
                Id = id,
                Uri = uri
            };
        });

你可以这样写

var result1 = await ioBoundTask1;
var result2 = await ioBoundTask2;

var id = result1.Id;
var uri = result2.Uri;
            
doSomethingWithIdAndUri(id, uri);
            
return new IOBoundTaskResult
{
    Id = id,
    Uri = uri
};

这使得正在发生的事情更清楚,并且更容易避免异常被抛出到错误的地方/根本没有抛出等问题。有些情况下需要更明确地使用延续,但通常应该避免使用更清晰、更简洁的 async/await 语法。

编辑:Here's a link to my version of the code,异常应该正确地“冒泡”。