在异步中异常处理的意外行为,可能的错误?

时间:2012-01-19 09:23:50

标签: asynchronous exception-handling f# async-workflow

我在调用嵌套的异步时偶然发现了一个问题。引发了异常,但无法使用异步工作流提供的任何常规异常处理方法。

以下是一个简单的测试,可以重现问题:

[<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.CatchAsync.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)或者使用反射来生成值(在我的例子中它是一个涉及的模拟框架) 。因此,这不是一个真正的错误,而是一个边缘案例。

2 个答案:

答案 0 :(得分:4)

我不认为这是一个错误。正如名称所示Unchecked.defaultof<_>不会检查它生成的值是否有效,而Async<unit>不支持null作为正确的值(例如,如果您尝试使用{{请参阅消息} 1}})。 let x : Async<unit> = null等旨在捕获异步计算中抛出的异常,而不是由于在编译器后面隐藏并创建无效的异步计算而导致的异常。

答案 1 :(得分:4)

我完全同意kvb - 当你使用Unchecked.defaultOf初始化一个值时,这意味着使用该值的行为可能是未定义的,因此不能将其视为bug。在实践中,您不必担心它,因为您永远不应该获得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