在异步计算表达式中使用“use”绑定时,为什么会延迟资源处置?

时间:2014-03-26 21:01:39

标签: asynchronous f# mailboxprocessor

我已经设置了一个代理,我在后台设置了一些数据库工作。实现看起来像这样:

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会产生相同的行为?

1 个答案:

答案 0 :(得分: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次&#34; bye&#34; - 这是因为在递归调用期间分配的资源仍在范围内。

在您的示例中,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) }