Почему удаление ресурсов задерживается при использовании привязки "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) }