我有一个代理,我设置在后台做一些数据库工作。实现看起来像这样:
let myAgent = MailboxProcessor<AgentData>.Start(fun inbox ->
let rec loop =
async {
let! data = inbox.Receive()
use conn = new System.Data.SqlClient.SqlConnection("...")
data |> List.map (fun e -> // Some transforms)
|> List.sortBy (fun (_,_,t,_,_) -> t)
|> List.iter (fun (a,b,c,d,e) ->
try
... // Do the database work
with e -> Log.error "Yikes")
return! loop
}
loop)
有了这个,我发现如果在一段时间内调用几次,我就会开始让SqlConnection对象堆积起来,而不是被处置,最终我将耗尽连接池中的连接(我没有确切的指标来衡量"几个"是多少,但是连续运行两次集成测试套件总是会导致连接池干涸)。
如果我将use
改为using
,那么事情就会被妥善处理,我就没有问题了:
let myAgent = MailboxProcessor<AgentData>.Start(fun inbox ->
let rec loop =
async {
let! data = inbox.Receive()
using (new System.Data.SqlClient.SqlConnection("...")) <| fun conn ->
data |> List.map (fun e -> // Some transforms)
|> List.sortBy (fun (_,_,t,_,_) -> t)
|> List.iter (fun (a,b,c,d,e) ->
try
... // Do the database work
with e -> Log.error "Yikes")
return! loop
}
loop)
似乎AsyncBuilder的Using
方法由于某种原因没有正确调用其最终函数,但不清楚为什么。这是否与我写递归异步表达式的方式有关,或者这是一些模糊的错误?这是否意味着在其他计算表达式中使用use
可以产生相同的行为?
这实际上是预期的行为-尽管不是完全明显!
当异步工作流的执行离开当前作用域时,use
构造处置资源。这与异步工作流之外的use
的行为相同。问题是递归调用(在async外部)或使用return!
递归调用(在async内部)并不意味着您正在离开作用域。因此,在这种情况下,资源只在递归调用返回之后被处置。
为了测试这一点,我将使用一个helper,它在被丢弃时输出:
let tester () =
{ new System.IDisposable with
member x.Dispose() = printfn "bye" }
下面的函数在10次迭代后终止递归。这意味着只有在整个工作流完成后,它才会继续分配资源并处置所有资源:
let rec loop(n) = async {
if n < 10 then
use t = tester()
do! Async.Sleep(1000)
return! loop(n+1) }
如果你运行这个,它将运行10秒,然后打印10次"bye"——这是因为在递归调用期间分配的资源仍然在作用域中。
在您的示例中,using
函数更明确地划分了范围。但是,您可以使用嵌套异步工作流来完成相同的工作。以下代码只在调用Sleep
方法时才在作用域中拥有资源,因此它在递归调用之前将其处理:
let rec loop(n) = async {
if n < 10 then
do! async {
use t = tester()
do! Async.Sleep(1000) }
return! loop(n+1) }
同样,当您使用for
循环或其他限制作用域的结构时,资源将立即被处置:
let rec loop(n) = async {
for i in 0 .. 10 do
use t = tester()
do! Async.Sleep(1000) }