Неожиданное поведение с обработкой исключений в асинхронном режиме, возможная ошибка?
Я наткнулся на проблему при вызове вложенного Async, который оказывается нулевым. Возникает исключение, но оно не может быть перехвачено ни одним из обычных методов обработки исключений, которые обеспечивают асинхронные рабочие процессы.
Ниже приведен простой тест, который воспроизводит проблему:
[<Test>]
let ``Nested async is null with try-with``() =
let g(): Async<unit> = Unchecked.defaultof<Async<unit>>
let f = async {
try
do! g()
with e ->
printf "%A" e
}
f |> Async.RunSynchronously |> ignore
что приводит к следующему исключению:
System.NullReferenceException : Object reference not set to an instance of an object.
at Microsoft.FSharp.Control.AsyncBuilderImpl.bindA@714.Invoke(AsyncParams`1 args)
at <StartupCode$FSharp-Core>.$Control.loop@413-40(Trampoline this, FSharpFunc`2 action)
at Microsoft.FSharp.Control.Trampoline.ExecuteAction(FSharpFunc`2 firstAction)
at Microsoft.FSharp.Control.TrampolineHolder.Protect(FSharpFunc`2 firstAction)
at Microsoft.FSharp.Control.AsyncBuilderImpl.startAsync(CancellationToken cancellationToken, FSharpFunc`2 cont, FSharpFunc`2 econt, FSharpFunc`2 ccont, FSharpAsync`1 p)
at Microsoft.FSharp.Control.CancellationTokenOps.starter@1121-1.Invoke(CancellationToken cancellationToken, FSharpFunc`2 cont, FSharpFunc`2 econt, FSharpFunc`2 ccont, FSharpAsync`1 p)
at Microsoft.FSharp.Control.CancellationTokenOps.RunSynchronously(CancellationToken token, FSharpAsync`1 computation, FSharpOption`1 timeout)
at Microsoft.FSharp.Control.FSharpAsync.RunSynchronously(FSharpAsync`1 computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken)
at Prioinfo.Urkund.DocCheck3.Core2.Tests.AsyncTests.Nested async is null with try-with() in SystemTests.fs: line 345
Я действительно думаю, что исключение должно быть поймано в этом случае, или это действительно ожидаемое поведение? (Я использую Visual Studio 2010 Sp1 для записи)
Также, Async.Catch
а также Async.StartWithContinuations
демонстрирует ту же проблему, что и эти тестовые примеры:
[<Test>]
let ``Nested async is null with Async.Catch``() =
let g(): Async<unit> = Unchecked.defaultof<Async<unit>>
let f = async {
do! g()
}
f |> Async.Catch |> Async.RunSynchronously |> ignore
[<Test>]
let ``Nested async is null with StartWithContinuations``() =
let g(): Async<unit> = Unchecked.defaultof<Async<unit>>
let f = async {
do! g()
}
Async.StartWithContinuations(f
, fun _ -> ()
, fun e -> printfn "%A" e
, fun _ -> ())
Кажется, исключение возникает в методе связывания в конструкторе рабочих процессов, и я предполагаю, что в результате обычный код обработки ошибок будет обойден. Это похоже на ошибку в реализации асинхронных рабочих процессов, так как я не нашел ничего в документации или где-либо еще, чтобы предположить, что это - предполагаемое поведение.
В большинстве случаев обойти это довольно легко, я думаю, что это, по крайней мере, не большая проблема для меня, но это немного тревожит, поскольку это означает, что вы не можете полностью доверять механизму обработки исключений асинхронного режима, чтобы иметь возможность перехватывать все исключения,
Редактировать:
Подумав немного, я согласен с kvb. Нулевые асинхронности в действительности не должны существовать в обычном коде и могут быть созданы только в том случае, если вы делаете что-то, чего, вероятно, не должны (например, используете Unchecked.defaultOf) или используете отражение для получения значений (в моем случае это была фреймворк), Таким образом, это не совсем ошибка, а скорее крайний случай.
2 ответа
Я не думаю, что это ошибка. Как видно из названия Unchecked.defaultof<_>
не проверяет, что значения, которые он производит, действительны, и Async<unit>
не поддерживается null
как правильное значение (например, см. сообщение, если вы пытаетесь использовать let x : Async<unit> = null
). Async.Catch
и тому подобное предназначены для перехвата исключений, генерируемых в асинхронных вычислениях, а не исключений, вызванных прократием за спиной компилятора и созданием недопустимых асинхронных вычислений.
Я полностью согласен с kvb - когда вы инициализируете значение с помощью Unchecked.defaultOf
, это означает, что поведение использования значения может быть неопределенным, поэтому это нельзя рассматривать как ошибку. На практике вам не нужно беспокоиться об этом, потому что вы никогда не должны получать null
значения Async<'T>
тип.
Чтобы добавить больше деталей, исключение не может быть обработано, потому что перевод выглядит следующим образом:
async.TryWith
( async.Bind ( Unchecked.defaultof<_>,
fun v -> async { printfn "continued" } ),
fun e -> printfn "%A" e)
Исключение выбрасывается из Bind
метод до рабочего процесса, возвращенного Bind
запускается (это происходит после того, как вы позвоните RunSynchronously
потому что рабочий процесс упакован с использованием Delay
, но это происходит вне выполнения рабочего процесса). Если вы хотите обрабатывать такого рода исключения (возникающие из-за неправильно сконструированных рабочих процессов), вы можете написать версию TryWith
который запускает рабочий процесс и обрабатывает исключения, выданные за пределами выполнения:
let TryWith(work, handler) =
Async.FromContinuations(fun (cont, econt, ccont) ->
try
async { let! res = work in cont res }
|> Async.StartImmediate
with e ->
async { let! res = handler e in cont res }
|> Async.StartImmediate )
Затем вы можете обрабатывать исключения следующим образом:
let g(): Async<unit> = Unchecked.defaultof<Async<unit>>
let f =
TryWith
( (async { do! g() }),
(fun e -> async { printfn "error %A" e }))
f |> Async.RunSynchronously