【问题标题】:Why does this IDisposable get disposed while referenced in a F# task?为什么在 F# 任务中引用此 IDisposable 时会被释放?
【发布时间】:2019-12-28 21:22:26
【问题描述】:

不久前,在我们的一个 (X) 单元测试中,我的一位同事写道:

[<Fact>]
let ``Green Flow tests`` () =
    use factory = new WebAppFactory()
    use client = factory.CreateClient()

    Check.theGreenFlow client
    |> Async.AwaitTask
    |> Async.RunSynchronously

很惊讶,我想知道为什么我的同事强制调用 Async.RunSynchronously 而 XUnit 可以很好地处理 TaskAsync 类型。

然后我试了一下:

[<Fact>]
let ``Green Flow tests`` () =
    use factory = new WebAppFactory()
    use client = factory.CreateClient()

    // this btw returns Task<unit>
    Check.theGreenFlow client

得到:

Rm.Bai.IntegrationTests.RetrievalWorkflow.Green Flow tests

System.AggregateException : One or more errors occurred. (One or more errors occurred. (One or more errors occurred. (One or more errors occurred. (One or more errors occurred. (One or more errors occurred. (Cannot access a disposed object.
Object name: 'IServiceProvider'.))))))

我想“很公平,use 的范围在函数的底部结束,然后在 Task&lt;unit&gt; 由 XUnit 运行器处理时释放”。

尽管IDisposable 对象可能在上面示例中返回Task 的函数中被引用,但运行器在函数结束后运行任务,因此根据我的说法,Dispose 调用已经发生了解https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/resource-management-the-use-keyword

它提供与let 绑定相同的功能,但在值超出范围时添加对Dispose 的调用。请注意,编译器会在值上插入null 检查,因此如果值为null,则不会尝试调用 Dispose。

[...]

当您使用use 关键字时,Dispose 在包含代码块的末尾被调用

对我来说,避免处置对象的正确方法是使用 taskasync 计算表达式之类的计算表达式:

[<Fact>]
let ``Green Flow tests`` () =
    task {
        use factory = new WebAppFactory()
        use client = factory.CreateClient()

        do! Check.theGreenFlow client
    }

因此,Dispose() 实际上是代码的时刻被明确定义。 并且不必像在 sn-p no 中那样强制测试同步运行。 1.

与下面的内容不同,它仍然会导致 Cannot access a disposed object. 错误:

[<Fact>]
let ``Green Flow tests`` () =
    use factory = new WebAppFactory()
    use client = factory.CreateClient()

    async {
        do! Check.theGreenFlow client |> Async.AwaitTask
    }

类似于 sn-p 号。 2.

我对这个问题的理解正确吗?

【问题讨论】:

  • 是的,你的理解是正确的。

标签: .net-core f# task xunit


【解决方案1】:

下一个实验演示第二个示例的流程(当最后有 Check.theGreenFlow 客户端时)。 F#:

let tst(runSvc:Func<_,_>) =  
    printfn "[%d] start tst" Thread.CurrentThread.ManagedThreadId     
    use srv = {new IDisposable with 
                    override x.Dispose() = 
                        printfn "[%d] srv disposed" Thread.CurrentThread.ManagedThreadId }
    let res: Task = runSvc.Invoke srv
    printfn "[%d] end tst" Thread.CurrentThread.ManagedThreadId 
    res

这是 Green Flow 测试的模拟。 C#:

static async Task runSvc(IDisposable svc) {
    Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] before runSvc");
    await Task.Delay(1000);
    Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] after runSvc");
}
static async Task Main(string[] args)
{
    var task = T1.tst(runSvc); 
    await task;
}

runSvc 扮演 Check.theGreenFlow 的角色和 Main - XUnit test run 的角色。

输出:

[1] start tst
[1] before runSvc
[1] end tst
[1] srv disposed
[4] after runSvc 
  1. 第一个线程开始测试并进入 Check.theGreenFlow。
  2. Check.theGreenFlow 在某个时间点为异步操作注册了一个延续 - 在本示例中,它位于 task.Delay 点。
  3. 继续注册后,Check.theGreenFlow 立即返回对象 Task(在我的示例中,继续打印“runSvc 之后”,这部分为以后注册)。
  4. 当代码从测试函数返回时,服务对象被放置在最后,线程 1 等待结果,在某个时候将由线程池中注册的延续创建(另一个线程 4 执行“runSvc 之后”)。如果线程 4 继续访问服务对象,则会发生对象处理异常。
  5. 可能存在竞争条件。如果在我上面的示例中,您将 Thread.Sleep(2000) 放在“[%d] end tst”之前,则“在 runSvc 之后”将在处理之前继续执行,并且没有例外。

【讨论】:

    猜你喜欢
    • 2011-05-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-23
    • 1970-01-01
    • 2019-01-08
    • 2012-12-22
    • 2015-03-10
    相关资源
    最近更新 更多