Почему отгрузка ресурсов будет отложена при использовании привязки "use" в выражении вычисления async?

У меня есть агент, который я создал для работы с базой данных в фоновом режиме. Реализация выглядит примерно так:

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 на a 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)

Похоже, что метод using для AsyncBuilder неправильно вызвал его функцию finally по какой-то причине, но непонятно почему. Это связано с тем, как я написал свое рекурсивное выражение async или это какая-то непонятная ошибка? И предполагает ли это, что использование use в других выражениях вычислений может привести к такому же поведению?

Ответ 1

Это на самом деле ожидаемое поведение - хотя и не совсем очевидное!

Конструкция use предоставляет ресурс, когда выполнение асинхронного рабочего процесса выходит из текущей области. Это то же самое, что и поведение use вне асинхронных рабочих процессов. Проблема в том, что рекурсивный вызов (вне асинхронного) или рекурсивный вызов с использованием return! (внутри async) не означает, что вы покидаете область видимости. Таким образом, в этом случае ресурс удаляется только после возврата рекурсивного вызова.

Чтобы проверить это, я использую помощник, который печатает при удалении:

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 раз "пока" - это связано с тем, что выделенные ресурсы по-прежнему находятся в области во время рекурсивных вызовов.

В вашем примере функция 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) }