Неожиданное поведение с обработкой исключений в асинхронном режиме, возможная ошибка? - PullRequest
3 голосов
/ 19 января 2012

Я наткнулся на проблему при вызове вложенного 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 ]

4 голосов
/ 19 января 2012

Я полностью согласен с 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
4 голосов
/ 19 января 2012

Я не думаю, что это ошибка. Как видно из названия, Unchecked.defaultof<_> не проверяет правильность полученных значений, а Async<unit> не поддерживает null в качестве правильного значения (например, см. Сообщение, если вы пытаетесь использовать let x : Async<unit> = null). Async.Catch и т.п. предназначены для перехвата исключений, возникающих в асинхронных вычислениях, а не исключений, вызванных прократием за спиной компилятора и созданием недопустимых асинхронных вычислений.

...