在 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
实例,而是让 addStuff
和 updateStuff
创建他们自己的 MyClient
实例。
在这种情况下,async { ... }
块为您提供的是两件事:
- 它延迟执行其中所有代码,直到执行异步计算,
- 它相应地处理
use
关键字(即 IDisposable 将在嵌套的 addStuff
/updateStuff
工作流执行后被释放)。
至于您使用的模式是否错误-是的。
F# async 和 C# async-await 是两种截然不同的结构,具有截然不同的语义,并且一个人的经验不容易移植到另一个人身上。
C# async-await 是一种链接方式 Tasks
。 Task<'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>
值给调用者。然后调用者可以自由地做任何他们想做的事——执行它,忽略它,或者不执行而进一步传递它。
它失败的原因是因为 myClient
在 doStuff
returns 时被处理 - 这是在调用者有机会执行异步之前。
问题来自这样一个事实,即您使用的这种模式将特定逻辑片段的执行分为两部分 - 一部分在调用函数时执行,另一部分在异步执行时执行,而代码是用那里的一切都作为一个整体执行的意图。
这是一种或多或少会引起细微错误的模式,即使在许多情况下不容易观察到这种差异的影响——尤其是在函数 returns 之后立即无条件执行异步的情况下.
当 运行 以下代码时出现 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
实例,而是让 addStuff
和 updateStuff
创建他们自己的 MyClient
实例。
在这种情况下,async { ... }
块为您提供的是两件事:
- 它延迟执行其中所有代码,直到执行异步计算,
- 它相应地处理
use
关键字(即 IDisposable 将在嵌套的addStuff
/updateStuff
工作流执行后被释放)。
至于您使用的模式是否错误-是的。
F# async 和 C# async-await 是两种截然不同的结构,具有截然不同的语义,并且一个人的经验不容易移植到另一个人身上。
C# async-await 是一种链接方式 Tasks
。 Task<'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>
值给调用者。然后调用者可以自由地做任何他们想做的事——执行它,忽略它,或者不执行而进一步传递它。
它失败的原因是因为 myClient
在 doStuff
returns 时被处理 - 这是在调用者有机会执行异步之前。
问题来自这样一个事实,即您使用的这种模式将特定逻辑片段的执行分为两部分 - 一部分在调用函数时执行,另一部分在异步执行时执行,而代码是用那里的一切都作为一个整体执行的意图。
这是一种或多或少会引起细微错误的模式,即使在许多情况下不容易观察到这种差异的影响——尤其是在函数 returns 之后立即无条件执行异步的情况下.