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

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

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)

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

1 ответ

Решение

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

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

В вашем образце 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) }
Другие вопросы по тегам