在 F# 中使用 IDisposable 是否需要使用 async 关键字?

Is it necessary to use async keyword when using IDisposable in F#?

当 运行 以下代码时出现 Cannot access a disposed object 错误(MyClient 是由 C# 项目中的服务引用生成的 WCF 客户端)。

type Action =
    | Add
    | Update

let addStuff (myClient:MyClient) arg = async {
    let! response = myClient.AddAsync arg |> Async.AwaitTask
    return response.ID
}

let updateStuff (myClient:MyClient) arg = async {
    let! response = myClient.UpdateAsync arg |> Async.AwaitTask
    return response.ID
}

let doStuff arg =
    use myClient = new MyClient()
    match arg.Action with
    | Add -> arg |> addStuff myClient
    | Update -> arg |> updateStuff myClient

let args = [Add, Add, Update, Add]

let results =
    args
    |> List.map doStuff
    |> Async.Parallel

客户端似乎比我预期的要处理得早。如果我将 doStuff 更改为:

let doStuff arg = async {
    use myClient = new MyClient()
    return!
        match arg.Action with
        | Add -> arg |> addStuff myClient
        | Update -> arg |> updateStuff myClient
}

两个函数的return类型都是Async<int>。为什么客户端在第一个示例中提前处理?我认为这两个例子在逻辑上是相同的。我的理解是 async 工作流只有在您需要使用 ! 绑定时才是必需的,我认为在这种情况下不需要,因为实际等待发生在特定函数中。

问题出在 doStuff:

let doStuff arg =
    use myClient = new MyClient()
    match arg.Action with
    | Add -> arg |> addStuff myClient
    | Update -> arg |> updateStuff myClient

您正在将 myClient 传递给捕获 MyClient 实例的异步函数。但是,当 doStuff returns 时,它会在 MyClient 实例上调用 Dispose 并释放客户端。当您的异步方法到达 运行 时,它正在使用已处置的实例。

使 doStuff 有效,因为处置成为异步工作流程的一部分。

另一种选择是不 use MyClient 实例,而是让 addStuffupdateStuff 创建他们自己的 MyClient 实例。

在这种情况下,async { ... } 块为您提供的是两件事:

  1. 它延迟执行其中所有代码,直到执行异步计算,
  2. 它相应地处理 use 关键字(即 IDisposable 将在嵌套的 addStuff/updateStuff 工作流执行后被释放)。

至于您使用的模式是否错误-是的。

F# async 和 C# async-await 是两种截然不同的结构,具有截然不同的语义,并且一个人的经验不容易移植到另一个人身上。

C# async-await 是一种链接方式 TasksTask<'t> 是一个未来,即稍后可用的 't 类型值的容器。默认情况下,生成该值的计算会立即安排在后台线程上执行,访问该值是一个阻塞操作,直到该计算完成,并进一步尝试访问它 return 缓存值。

另一方面,F# Async<'t> 是表示计算的抽象规范的值,一旦执行,就会产生类型 't 的值。然而,执行被推迟到调用者——调用者可以选择如何(如果有的话)执行计算。与任务不同,异步不携带类型 't 的值 - 每次执行都会产生一个新的(可能不同的)值。

回到你的样本:

let doStuff arg =
    use myClient = new MyClient()
    match arg.Action with
    | Add -> arg |> addStuff myClient
    | Update -> arg |> updateStuff myClient

你这里有一个函数,它可以做一些工作并且 return 是一个 Async<'t> 值给调用者。然后调用者可以自由地做任何他们想做的事——执行它,忽略它,或者不执行而进一步传递它。

它失败的原因是因为 myClientdoStuff returns 时被处理 - 这是在调用者有机会执行异步之前。

问题来自这样一个事实,即您使用的这种模式将特定逻辑片段的执行分为两部分 - 一部分在调用函数时执行,另一部分在异步执行时执行,而代码是用那里的一切都作为一个整体执行的意图。

这是一种或多或少会引起细微错误的模式,即使在许多情况下不容易观察到这种差异的影响——尤其是在函数 returns 之后立即无条件执行异步的情况下.