F#Async.Run与timeout和cancellationToken同步

Joe*_*lor 4 f# async-workflow cancellation-token

使用超时和CancellationToken调用Async.RunSynchronously时,似乎忽略超时值.我可以通过在CancellationToken上调用CancelAfter来解决这个问题,但理想情况下我希望能够区分工作流中出现的异常,TimeOutExceptions和OperationCanceledExceptions.

我相信下面的示例代码演示了这一点.

open System
open System.Threading

let work = 
    async {
        let endTime = DateTime.UtcNow.AddMilliseconds(100.0)
        while DateTime.UtcNow < endTime do
            do! Async.Sleep(10)
            Console.WriteLine "working..."
        raise ( Exception "worked for more than 100 millis" )
    }


[<EntryPoint>]
let main argv = 
    try
        Async.RunSynchronously(work, 50)
    with
        | e -> Console.WriteLine (e.GetType().Name + ": " + e.Message)

    let cts = new CancellationTokenSource()

    try
        Async.RunSynchronously(work, 50, cts.Token)
    with
        | e -> Console.WriteLine (e.GetType().Name + ": " + e.Message)  


    cts.CancelAfter(80)
    try
        Async.RunSynchronously(work, 50, cts.Token)
    with
        | e -> Console.WriteLine (e.GetType().Name + ": " + e.Message)  

    Console.ReadKey(true) |> ignore

    0
Run Code Online (Sandbox Code Playgroud)

输出如下,表明超时仅在第一种情况下有效(未指定CancelationToken)

working...
working...
TimeoutException: The operation has timed out.
working...
working...
working...
working...
working...
working...
working...
Exception: worked for more than 100 millis
working...
working...
working...
working...
working...
working...
OperationCanceledException: The operation was canceled.
Run Code Online (Sandbox Code Playgroud)

这是预期的行为吗?有没有办法得到我追求的行为?

谢谢!

Tom*_*cek 10

我不确定这是否是预期的行为 - 至少,我认为没有任何理由.但是,此行为直接在处理参数时实现RunSynchronously.如果查看库源代码,可以看到:

static member RunSynchronously (p:Async<'T>,?timeout,?cancellationToken) =
  let timeout,token =
    match cancellationToken with
    | None -> timeout,(!defaultCancellationTokenSource).Token                
    | Some token when not token.CanBeCanceled -> timeout, token                
    | Some token -> None, token
Run Code Online (Sandbox Code Playgroud)

在您的情况下(具有可以取消的超时和取消令牌),代码将通过最后一个分支并忽略超时.我认为这是一个错误,或者它应该在文档中提到.

作为一种变通方法,您可以创建一个单独CancellationTokenSource的指定超时并将其链接到主取消源,以便调用者提供(使用CreateLinkedTokenSource).获得后OperationCancelledException,您可以检测源是实际取消还是超时:

type Microsoft.FSharp.Control.Async with
  static member RunSynchronouslyEx(a:Async<'T>, timeout:int, cancellationToken) =
    // Create cancellation token that is cancelled after 'timeout'
    let timeoutCts = new CancellationTokenSource()
    timeoutCts.CancelAfter(timeout)

    // Create a combined token that is cancelled either when 
    // 'cancellationToken' is cancelled, or after a timeout
    let combinedCts = 
      CancellationTokenSource.CreateLinkedTokenSource
        (cancellationToken, timeoutCts.Token)

    // Run synchronously with the combined token
    try Async.RunSynchronously(a, cancellationToken = combinedCts.Token)
    with :? OperationCanceledException as e ->
      // If the timeout occurred, then we throw timeout exception instead
      if timeoutCts.IsCancellationRequested then
        raise (new System.TimeoutException())
      else reraise()
Run Code Online (Sandbox Code Playgroud)