[英]Exception thrown in nested async / await functions causes unexpected behavior
[英]Unexpected behavior with exception handling in async, possible bug?
我在調用嵌套的異步時偶然發現了一個問題。 引發了異常,但無法使用異步工作流提供的任何常規異常處理方法。
以下是一個簡單的測試,它可以重現這個問題:
[<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 _ -> ())
似乎在工作流構建器中的bind-method中引發了異常,我的猜測是,因此繞過了正常的錯誤處理代碼。 它似乎是異步工作流程實現中的一個錯誤,因為我沒有在文檔或其他地方發現任何暗示這是預期行為的內容。
在大多數情況下我很容易解決這個問題,所以至少對我來說這不是一個大問題,但它有點令人不安,因為它意味着你不能完全信任異步異常處理機制,以便能夠捕獲所有異常。
編輯:
在考慮之后我同意kvb。 Null asyncs實際上不應該存在於普通代碼中,只有當你做了一些你可能不應該做的事情時(例如使用Unchecked.defaultOf)或者使用反射來生成值(在我的例子中它是一個涉及的模擬框架) 。 因此,這不是一個真正的錯誤,而是一個邊緣案例。
我不認為這是一個錯誤。 由於名稱指示Unchecked.defaultof<_>
不檢查它生成的值是否有效,並且Async<unit>
不支持null
作為正確值(例如,如果您嘗試使用let x : Async<unit> = null
請參閱消息let x : Async<unit> = null
)。 Async.Catch
等旨在捕獲異步計算中拋出的異常,而不是在編譯器后面隱藏並創建無效異步計算所導致的異常。
我完全同意kvb - 當你使用Unchecked.defaultOf
初始化一個值時,這意味着使用該值的行為可能是未定義的,因此不能將其視為bug。 在實踐中,您不必擔心它,因為您永遠不應該獲得Async<'T>
類型的null
值。
要添加更多詳細信息,無法處理異常,因為翻譯如下所示:
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
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.