如何在C#中处理异步方法和IDisposable?

8
我有一些使用xUnit的集成测试需要在测试期间删除一些资源。为此,我已经在包含测试的类中实现了IDisposable
问题是,我需要使用仅具有异步接口的客户端来删除测试期间创建的资源。但是,Dispose方法是同步的。
我可以使用.Result.Wait()等待异步调用的完成,但这可能会创建死锁(该问题在此处有详细记录)。
鉴于我不能使用.Result.Wait(),那么在Dispose方法中调用异步方法的正确(且安全)方式是什么?
更新:添加(简化的)示例以显示问题。
[Collection("IntegrationTests")]
public class SomeIntegrationTests : IDisposable {
    private readonly IClient _client; // SDK client for external API

    public SomeIntegrationTests() {
        // initialize client
    }

    [Fact]
    public async Task Test1() {
        await _client
            .ExecuteAsync(/* a request that creates resources */);

        // some assertions
    }

    public void Dispose() {
        _client
            .ExecuteAsync(/* a request to delete previously created resources */)
            .Wait(); // this may create a deadlock
    }
}

你能展示一下你的异步释放方法吗?我还没有听说过一个可以异步释放自己的接口。在类被释放之前,你是否绝对需要等待异步释放完成?异步对象终结器尚未实现。https://github.com/dotnet/coreclr/issues/22598 - Avin Kavish
@AvinKavish 刚刚添加了一个示例。 - betabandido
我猜删除逻辑是针对测试方法唯一的?将“teardown”逻辑移动到测试本身应该没问题。xUnit的作者告诉你为什么。https://jamesnewkirk.typepad.com/posts/2007/09/why-you-should-.html - Avin Kavish
1
你是否真的在使用.GetAwaiter().GetResult()这样的东西时遇到了问题,还是只是想要预防问题?如果你不是在同步上下文(UI应用程序..)中运行,那么情况并不会太糟糕。如果你不需要“等待”处理,你也可以触发异步执行而不必等待它完成 - 例如async void - 只需确保通过添加try/catch来避免引起未处理的异常。 - Martin Ullrich
2
如果您绝对需要,可以在xunit上提交问题以支持 .net core 3.0 中即将推出的 IAsyncDisposable - Martin Ullrich
@MartinUllrich 在某些情况下使用这些方法可能会导致死锁。当使用xunit运行测试时,这种情况经常发生。 - betabandido
3个回答

11
原来xunit实际上包含了一些处理我遇到的问题的支持。测试类可以实现 IAsyncLifetime 接口以异步方式初始化和拆卸测试。该接口如下所示:
public interface IAsyncLifetime
{
    Task InitializeAsync();
    Task DisposeAsync();
}

虽然这是解决我特定问题的方法,但它并不能解决从 Dispose 调用异步方法的更普遍问题(当前的答案都没有解决这个问题)。我想我们需要等待直到 .NET core 3.0 中提供了 IAsyncDisposable(感谢 @MartinUllrich 提供此信息)才能解决这个问题。

4

我有类似的问题,特别是XUnit在这里表现得很棘手。 我通过将所有清理代码移动到测试中来“解决”了这个问题,例如try..finally块。这样做不太优雅,但更加稳定,避免了异步释放。 如果你有很多测试,可以添加一个方法来减少样板代码。

例如:

        private async Task WithFinalizer(Action<Task> toExecute)
    {

        try
        {
            await toExecute();
        }
        finally
        {
           // cleanup here
        }
    }

    // Usage
    [Fact]
    public async Task TestIt()
    {
        await WithFinalizer(async =>
        {
         // your test
         });
    }

另一个好处是,根据我的经验,清理工作通常高度依赖于测试 - 使用这种技术为每个测试提供自定义终结器会更容易(添加第二个操作可以用作终结器)。

我只是希望能够避免所有这些样板代码。我想这实际上相当不错。但是,我刚刚发现xunit提供了一些支持来处理我面临的问题 :) - betabandido

0
一个测试类执行几个相互关联的测试。测试类通常测试一个类或一组密切协作的类。有时,测试类仅测试一个函数。
通常,应设计测试,使其不依赖于其他测试:测试A应该成功而无需运行测试B,反之亦然:测试可能不假设其他测试的任何内容。
通常,测试会创建一些前提条件,调用要测试的函数,并检查是否满足后置条件。因此,每个测试通常都会创建自己的环境。
如果一堆测试需要类似的环境,则为节省测试时间,通常会为所有这些测试创建环境,运行测试并处置环境。这就是您在测试类中所做的。
然而,如果在您的测试中创建了一个任务来调用异步函数,则应在该测试中等待该任务的结果。如果不这样做,您将无法测试异步函数是否按预期执行其任务,即:"创建一个任务,当等待时返回..."。
void TestA()
{
    Task taskA = null;
    try
    {
        // start a task without awaiting
        taskA = DoSomethingAsync();
        // perform your test
        ...
        // wait until taskA completes
        taskA.Wait();
        // check the result of taskA
        ...
     }
     catch (Exception exc)
     {
         ...
     }
     finally
     {
         // make sure that even if exception TaskA completes
         taskA.Wait();
     }
 }

结论:每个创建任务的测试方法都应该在完成之前等待这个类

在极少数情况下,您不需要等待任务完成就可以结束测试。也许是为了看看如果不等待任务会发生什么。我仍然认为这是一个奇怪的想法,因为这可能会影响其他测试,但是嘿,这是你的测试类。

这意味着,您的Dispose必须确保在测试方法结束时未完成的所有启动任务都已等待。

List<Task> nonAwaitedTasks = new List<Task>();

var TestA()
{
    // start a Task, for some reason you don't want to await for it:
    Task taskA = DoSomethingAsync(...);
    // perform your test

    // finish without awaiting for taskA. Make sure it will be awaited before the
    // class is disposed:
    nonAwaitedTasks.Add(taskA);
}

public void Dispose()
{
    Dispose(true);
}
protected void Dispose(bool disposing)
{
    if (disposing)
    {
        // wait for all tasks to complete
        Task.WaitAll(this.nonAwaitedTasks);
    }
}
}

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