当在异步计算表达式中使用"use"绑定时,为什么资源的处理会延迟?

10

我有一个代理程序,我设置它在后台执行一些数据库工作。实现大致如下:

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方法由于某些原因没有正确调用其finally函数,但不清楚原因。这是否与我编写的递归异步表达式有关,还是一些模糊的错误?这是否意味着在其他计算表达式中利用use可能会产生相同的行为?

这不应该是let rec loop()return! loop(),也就是一个函数而不是值吗? - Daniel
我从来没有百分之百确定它必须是一个函数而不是一个值。我过去曾经使用过两者,它们似乎都能正常工作,但可能存在微妙的差异,这可能会导致我的问题。 - ckramer
要明确的是,在这种情况下,我还没有尝试将值更改为函数...至少目前还没有。给我大约10分钟 :) - ckramer
在这种情况下,从值更改为函数似乎没有什么区别。不过想法不错。 - ckramer
1个回答

11

这其实是预期的行为 - 尽管不完全明显!

use构造在异步工作流程执行离开当前范围时释放资源。这与异步工作流程之外的use的行为相同。问题在于递归调用(在异步之外)或使用return!进行递归调用(在异步之内),并不意味着您正在离开该范围。因此,在这种情况下,只有在递归调用返回后才会处理资源。

为了测试这一点,我将使用一个打印被处理时机的帮助程序:

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

啊,好的,现在你直接指出来我才明白。我现在感觉有点傻,没能意识到递归调用发生在 use 绑定的作用域内。非常感谢。 - ckramer
@ckramer,我很高兴我的解释有意义。我花了一些时间才理解实际上正在发生的事情 - 所以这绝对是一个棘手的案例! - Tomas Petricek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接