可取消的 F# async main 传播异常
cancellable F# async main with exceptions propagated
我正在寻找像 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
正如 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 终止的屏幕截图。请注意此版本中 Async.RunSynchronously
之后的额外 printfn
,以及它如何不执行。
我需要赶到明天,所以这是我想出的解决方案。 F# Async 与 C# Async 的互操作有时会非常令人沮丧。请查阅。如果有更好的解决方案,我会很高兴。
我最终将我自己的异常处理程序传递给 Async.StartWithContinuations
,但由于它在同一个线程上启动,我将 do! Async.SwitchToThreadPool()
添加到 mainAsync
。这允许 CancelKeyPress
工作。如果不需要CancelKeyPress
,就不用另开帖了。
主要目标是确保正确处理一些宝贵的资源。 "safe haven" 将在发生异常时打印,控制台应用程序被 ctrl+c 取消或被杀死。从 Visual Studio 开始,您可以通过点击 运行 时弹出的控制台 window 上的关闭按钮来终止应用程序。异常和被取消的退出代码设置不同。
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.RunSchronously
可以与 CancellationToken
一起使用,而且确实如此。我将他的回答标记为解决方案。这会像我想要的那样爆炸,除了 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
我正在寻找像 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
正如 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 终止的屏幕截图。请注意此版本中 Async.RunSynchronously
之后的额外 printfn
,以及它如何不执行。
我需要赶到明天,所以这是我想出的解决方案。 F# Async 与 C# Async 的互操作有时会非常令人沮丧。请查阅。如果有更好的解决方案,我会很高兴。
我最终将我自己的异常处理程序传递给 Async.StartWithContinuations
,但由于它在同一个线程上启动,我将 do! Async.SwitchToThreadPool()
添加到 mainAsync
。这允许 CancelKeyPress
工作。如果不需要CancelKeyPress
,就不用另开帖了。
主要目标是确保正确处理一些宝贵的资源。 "safe haven" 将在发生异常时打印,控制台应用程序被 ctrl+c 取消或被杀死。从 Visual Studio 开始,您可以通过点击 运行 时弹出的控制台 window 上的关闭按钮来终止应用程序。异常和被取消的退出代码设置不同。
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.RunSchronously
可以与 CancellationToken
一起使用,而且确实如此。我将他的回答标记为解决方案。这会像我想要的那样爆炸,除了 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