可取消的F#异步主对象,传播了异常

时间:2018-07-17 19:15:36

标签: f# .net-core f#-async

我正在寻找像F#Async.Start这样的解决方案,但没有吞下异常。我希望控制台应用程序正常死于未处理的异常。这是一个.NET Core 2.1应用程序,我需要通过响应Linux SIGTERM和SIGINT信号来处置资源。如何修改此代码,使其传播kaboom!异常?

let mainAsync() =
    async {
        // blow up on purpose after a number of seconds
        let seconds = 10
        printfn "%d seconds and counting" seconds
        do! (System.TimeSpan.FromSeconds(float seconds)).TotalMilliseconds |> int |> Async.Sleep

        //failwithf "kaboom!"
        // Update: failwithf does reproduce the problem
        // My real app is making WCF calls that are Task based and awaiting on them
        // I don't know how to make a small test case for this
        // The client is throwing a TimeoutException if unable to connect

        wcfClient.RetrieveServiceContentAsync someValue |> Async.AwaitTask
    }

[<EntryPoint>]
let main argv =
    use cancelMainAsync = new System.Threading.CancellationTokenSource()
    use cancelMain = new System.Threading.AutoResetEvent(false)
    let cancel() =
        if not cancelMainAsync.IsCancellationRequested then
            cancelMainAsync.Cancel()
            cancelMain.Set() |> ignore
    System.Runtime.Loader.AssemblyLoadContext.Default.add_Unloading(fun _ -> cancel())
    System.Console.CancelKeyPress.Add (fun keyPress -> keyPress.Cancel <- true; cancel())
    Async.Start(mainAsync(), cancelMainAsync.Token)
    cancelMain.WaitOne() |> ignore
    0

2 个答案:

答案 0 :(得分:2)

正如Fyodor在评论中所述,我认为仅使用Async.RunSynchronously就可以解决问题。当我按下CTRL + C时,我放在一起的这段代码看起来好像打印了“ Cancelled”,并且抛出了“ Kaboom!”。不这样做的例外情况:

open System
open System.Threading

let f () = 
    async {
        printfn "Running..."
        do! Async.Sleep 10000
        failwith "Kaboom!"
    }

[<EntryPoint>]
let main argv =
    use cancellation = new CancellationTokenSource()
    Console.CancelKeyPress |> Event.add (fun _ -> cancellation.Cancel(); printfn "Cancelled")
    Async.RunSynchronously(f(), cancellationToken = cancellation.Token)
    0

编辑

该屏幕截图显示了CTRL + C终止。请注意此版本printfn之后的Async.RunSynchronously,以及它如何执行。

enter image description here

答案 1 :(得分:2)

我需要到明天,所以这是我想出的解决方案。在这种情况下,与C#异步进行的F#异步互操作可能会非常令人沮丧。请查阅。如果有更好的解决方案,我将感到高兴。

我最终将自己的异常处理程序传递给Async.StartWithContinuations,但是由于它是在同一线程上启动的,所以我在do! Async.SwitchToThreadPool()上添加了mainAsync。这样可以使CancelKeyPress正常工作。如果不需要CancelKeyPress,则无需将其放在另一个线程上。

主要目标是确保正确处置一些宝贵的资源。发生异常,使用ctrl + c取消控制台应用程序或杀死该控制台应用程序时,将打印“安全港”。在Visual Studio中,您可以通过单击运行时弹出的控制台窗口上的关闭按钮来终止该应用程序。退出代码的异常和取消设置有所不同。

let mainAsync() =
    async {
        do! Async.SwitchToThreadPool()

        use precious = { new System.IDisposable with override this.Dispose() = printfn "safe haven" }

        // blow up on purpose after a number of seconds
        let seconds = 10
        printfn "%d seconds and counting" seconds
        do! (System.TimeSpan.FromSeconds(float seconds)).TotalMilliseconds |> int |> Async.Sleep
        failwithf "kaboom!"

        // the real app is has a System.TimeoutException being thrown from a C# Task
        //wcfClient.RetrieveServiceContentAsync someValue |> Async.AwaitTask
    }

[<EntryPoint>]
let main argv =
    let mutable exitCode = 0
    use cancelMainAsync = new System.Threading.CancellationTokenSource()
    use cancelMain = new System.Threading.ManualResetEventSlim()

    let cancel() =
        if not cancelMainAsync.IsCancellationRequested then
            cancelMainAsync.Cancel()
            cancelMain.Set()

    let exceptionHandler (ex: System.Exception) =
        let ex =
            match ex with
            | :? System.AggregateException as ae ->
                if ae.InnerExceptions.Count = 1 then ae.InnerException else ex
            | _ -> ex
        printfn "%A" ex
        exitCode <- 1
        cancel()

    System.Runtime.Loader.AssemblyLoadContext.Default.add_Unloading(fun _ -> cancel())
    System.Console.CancelKeyPress.Add (fun args -> args.Cancel <- true; exitCode <- 2; cancel())
    Async.StartWithContinuations(mainAsync(), (fun _ -> ()), exceptionHandler, (fun _ -> ()), cancelMainAsync.Token)
    cancelMain.Wait()
    exitCode

使用解决方案更新

Aaron坚持认为Async.RunSchronouslyCancellationToken兼容并且可以。我把他的答案标记为解决方案。除了OperationCancelException以外,一切都会如我所愿。

open System

let mainAsync() =
    async {
        use precious = { new System.IDisposable with override this.Dispose() = printfn "safe haven" }
        // blow up on purpose after a number of seconds
        let seconds = 10
        printfn "%d seconds and counting" seconds
        do! (System.TimeSpan.FromSeconds(float seconds)).TotalMilliseconds |> int |> Async.Sleep
        failwithf "kaboom!"
        // the real app is has a System.TimeoutException being thrown from a C# Task
        //wcfClient.RetrieveServiceContentAsync someValue |> Async.AwaitTask
        return 0
    }

[<EntryPoint>]
let main argv =
    let cancellation = new Threading.CancellationTokenSource()
    let cancel() =
        if not cancellation.IsCancellationRequested then
            cancellation.Cancel()

    Runtime.Loader.AssemblyLoadContext.Default.add_Unloading(fun _ -> cancel())
    Console.CancelKeyPress.Add (fun event -> event.Cancel <- true; cancel())
    Async.RunSynchronously(mainAsync(), cancellationToken = cancellation.Token)

使用更多的异常处理更新解决方案

为完整起见,这是重新添加的其他异常处理。

open System

let mainAsync() =
    async {
        use precious = { new System.IDisposable with override this.Dispose() = printfn "safe haven" }
        // blow up on purpose after a number of seconds
        let seconds = 10
        printfn "%d seconds and counting" seconds
        do! (System.TimeSpan.FromSeconds(float seconds)).TotalMilliseconds |> int |> Async.Sleep
        failwithf "kaboom!"
        // the real app is has a System.TimeoutException being thrown from a C# Task
        //wcfClient.RetrieveServiceContentAsync someValue |> Async.AwaitTask
        return 0
    }

[<EntryPoint>]
let main argv =
    let cancellation = new Threading.CancellationTokenSource()
    let cancel() =
        if not cancellation.IsCancellationRequested then
            cancellation.Cancel()

    Runtime.Loader.AssemblyLoadContext.Default.add_Unloading(fun _ -> cancel())
    Console.CancelKeyPress.Add (fun event -> event.Cancel <- true; cancel())

    try
        Async.RunSynchronously(mainAsync(), cancellationToken = cancellation.Token)
    with
    | :? OperationCanceledException -> 2
    | ex ->
        let ex =
            match ex with
            | :? AggregateException as ae ->
                if ae.InnerExceptions.Count = 1 then ae.InnerException else ex
            | _ -> ex
        printfn "%A" ex
        1