在try/catch/finally中等待的良好解决方案是什么?

96

我需要在 catch 块中调用一个 async 方法,然后再像这样重新抛出异常(带有其堆栈跟踪):

try
{
    // Do something
}
catch
{
    // <- Clean things here with async methods
    throw;
}

但不幸的是,在catchfinally块中无法使用await。我了解到这是因为编译器没有办法在catch块中返回来执行await指令后面的内容之类的原因...

我尝试使用Task.Wait()替换await,但却造成了死锁。我在网上搜索了如何避免这种情况,并找到了这个网站

由于我不能更改async方法,也不知道它们是否使用ConfigureAwait(false),因此我创建了这些方法,它们接受一个Func<Task>,一旦我们进入另一个线程(以避免死锁),就会开始异步方法并等待其完成:

public static void AwaitTaskSync(Func<Task> action)
{
    Task.Run(async () => await action().ConfigureAwait(false)).Wait();
}

public static TResult AwaitTaskSync<TResult>(Func<Task<TResult>> action)
{
    return Task.Run(async () => await action().ConfigureAwait(false)).Result;
}

public static void AwaitSync(Func<IAsyncAction> action)
{
    AwaitTaskSync(() => action().AsTask());
}

public static TResult AwaitSync<TResult>(Func<IAsyncOperation<TResult>> action)
{
    return AwaitTaskSync(() => action().AsTask());
}

因此,我的问题是:您认为这段代码是否可行?

当然,如果您有其他改进方法或知道更好的方法,请告诉我! :)


2
在C# 6.0中实际上允许在catch块中使用await(请参见下面的答案)。 - Adi Lester
3
相关的C# 5.0错误信息:CS1985:无法在catch子句的主体中使用await。CS1984:无法在finally子句的主体中使用await。 - DavidRR
4个回答

178

如果需要的话,你可以将逻辑移到catch块之外,并使用ExceptionDispatchInfo重新抛出异常。

static async Task f()
{
    ExceptionDispatchInfo capturedException = null;
    try
    {
        await TaskThatFails();
    }
    catch (MyException ex)
    {
        capturedException = ExceptionDispatchInfo.Capture(ex);
    }

    if (capturedException != null)
    {
        await ExceptionHandler();

        capturedException.Throw();
    }
}

这样,当调用者检查异常的 StackTrace 属性时,它仍然记录了在 TaskThatFails 内部抛出的位置。


2
保留ExceptionDispatchInfo而不是Exception有什么优势(就像Stephen Cleary的回答中所述)? - Varvara Kalinina
3
如果您决定重新抛出异常,我猜想您将丢失之前的所有堆栈跟踪信息? - Varvara Kalinina
1
@VarvaraKalinina 没错。 - user743382

57

从 C# 6.0 开始,你可以在 catchfinally 块中使用 await,因此你实际上可以这样做:

try
{
    // Do something
}
catch (Exception ex)
{
    await DoCleanupAsync();
    throw;
}

包括我刚提到的新的C# 6.0特性都可以在这里列出或在视频中查看


C# 6.0中支持在catch/finally块中使用await也在维基百科上有所说明。 - DavidRR
5
@DavidRR,维基百科并不具有权威性。就这一点而言,它只是数以百万计的网站之一。 - Sam Hobbs
虽然这对于C# 6是有效的,但问题从一开始就被标记为C# 5。这让我想知道在这里提供这个答案是否会令人困惑,或者我们是否应该在这些情况下删除特定版本的标签。 - julealgon

16

如果您需要使用async错误处理程序,我建议像这样:

Exception exception = null;
try
{
  ...
}
catch (Exception ex)
{
  exception = ex;
}

if (exception != null)
{
  ...
}

同步阻塞异步代码的问题(无论在哪个线程上运行)是你会同步阻塞。在大多数情况下,最好使用await

更新:由于您需要重新抛出异常,因此可以使用ExceptionDispatchInfo


1
谢谢,但不幸的是我已经知道这个方法了。这通常是我所做的,但我在这里无法这样做。如果我只是在if语句中使用throw exception;,堆栈跟踪将会丢失。 - user2397050

3
我们从以下链接中提取了hvd的优秀回答,并在我们的项目中创建了下述可重用的实用类:

https://dev59.com/P2Qn5IYBdhLWcg3wtpBW#16626313

public static class TryWithAwaitInCatch
{
    public static async Task ExecuteAndHandleErrorAsync(Func<Task> actionAsync,
        Func<Exception, Task<bool>> errorHandlerAsync)
    {
        ExceptionDispatchInfo capturedException = null;
        try
        {
            await actionAsync().ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            capturedException = ExceptionDispatchInfo.Capture(ex);
        }

        if (capturedException != null)
        {
            bool needsThrow = await errorHandlerAsync(capturedException.SourceException).ConfigureAwait(false);
            if (needsThrow)
            {
                capturedException.Throw();
            }
        }
    }
}

使用方法如下:

    public async Task OnDoSomething()
    {
        await TryWithAwaitInCatch.ExecuteAndHandleErrorAsync(
            async () => await DoSomethingAsync(),
            async (ex) => { await ShowMessageAsync("Error: " + ex.Message); return false; }
        );
    }

请放心改进命名,我们故意保持了冗长的方式。请注意,不需要在包装器内部捕获上下文,因为它已经在调用点中捕获了,因此使用ConfigureAwait(false)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接