【问题标题】:How to deal with asynchronous methods and IDisposable in C#?C#中如何处理异步方法和IDisposable?
【发布时间】:2019-10-16 18:38:12
【问题描述】:

我有一些使用 xUnit 的集成测试需要拆除在测试期间创建的一些资源。为此,我在包含测试的类中实现了IDisposable

问题是我需要使用只有异步接口的客户端删除测试期间创建的资源。但是,Dispose 方法是同步的。

我可以使用.Result.Wait() 来等待异步调用的完成,但这可能会造成死锁(这个问题有据可查的here)。

鉴于我不能使用.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
    }
}

【问题讨论】:

  • 你能告诉我你的异步处理方法吗?还没有真正听说过异步处理自己的接口。在处理类之前,您绝对需要等待异步处理完成吗?异步对象终结器尚未实现。 github.com/dotnet/coreclr/issues/22598
  • @AvinKavish 刚刚添加了一个示例
  • 我猜删除逻辑是测试方法独有的?将teardown 逻辑移动到测试本身应该没问题。 xUnit 的作者告诉你原因。 jamesnewkirk.typepad.com/posts/2007/09/why-you-should-.html
  • 您在使用.GetAwaiter().GetResult() 之类的东西时是否真的遇到了问题,或者您是否试图避免出现问题?如果您不在同步上下文(UI 应用程序..)中运行,那还不错。如果您不需要“等待”处置,您也可以触发异步执行而不等待它完成 - 例如async void - 只需通过添加 try/catch 来确保不会导致未处理的异常
  • 如果您绝对需要,请在 xunit 上提交问题以支持即将在 .net core 3.0 中推出的IAsyncDisposable

标签: c# asp.net-core .net-core async-await xunit


【解决方案1】:

我也有类似的问题,尤其是 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 提供了一些支持来解决我面临的问题:)
【解决方案2】:

一个测试类执行多个相互关联的测试。一个测试类通常测试一个类或一组紧密协作的类。有时一个测试类只测试一个函数。

通常应该设计测试,使其不依赖于其他测试:测试 A 应该成功而不必运行测试 B,反之亦然:测试可能不假设其他测试的任何内容。

通常测试会创建一些前置条件,调用被测试的函数并检查是否满足后置条件。因此,每个测试通常都会创建自己的环境。

如果一堆测试需要一个类似的环境,为了节省测试时间,为所有这些测试创建一次环境、运行测试和处置环境是很常见的。这就是你在测试课中所做的。

但是,如果您在其中一个测试中创建了一个任务来调用异步函数,您应该在该测试中等待该任务的结果。如果不这样做,则无法测试 async 函数是否按预期执行,即:“创建一个等待时返回的任务......”。

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

结论:每个创建任务的Test方法都应该等待这个类完成后再完成

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

这意味着,您的 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);
    }
}
}

【讨论】:

    【解决方案3】:

    事实证明,xunit 实际上包含一些支持来处理我所面临的问题。测试类可以实现IAsyncLifetime 以异步方式初始化和拆除测试。界面如下:

    public interface IAsyncLifetime
    {
        Task InitializeAsync();
        Task DisposeAsync();
    }
    

    虽然这是针对我的特定问题的解决方案,但它并不能解决从 Dispose 调用异步方法的更通用问题(当前的答案也没有这样做)。我想为此我们需要等到IAsyncDisposable 在.NET core 3.0 中可用(感谢@MartinUllrich 提供此信息)。

    【讨论】:

      猜你喜欢
      • 2019-05-12
      • 1970-01-01
      • 1970-01-01
      • 2016-01-06
      • 2014-04-02
      • 1970-01-01
      • 2018-06-14
      • 1970-01-01
      • 2012-06-24
      相关资源
      最近更新 更多