F#Async <'T>取消的工作原理是什么?

vto*_*ola 10 f# cancellation f#-3.0 f#-async

我对使用TPL在C#中完成异步取消的方式非常满意,但我对F#有点困惑.通过呼叫显然Async.CancelDefaultToken()足以取消传出的Async<'T>操作.但它们没有像我预期的那样被取消,它们只是......消失了...我无法正确检测到取消并正确拆除堆栈.

例如,我有这个代码依赖于使用TPL的C#库:

type WebSocketListener with
  member x.AsyncAcceptWebSocket = async {
    let! client = Async.AwaitTask <| x.AcceptWebSocketAsync Async.DefaultCancellationToken
    if(not(isNull client)) then
        return Some client
    else 
        return None
  }

let rec AsyncAcceptClients(listener : WebSocketListener) =
  async {
    let! result = listener.AsyncAcceptWebSocket
    match result with
        | None -> printf "Stop accepting clients.\n"
        | Some client ->
            Async.Start <| AsyncAcceptMessages client
            do! AsyncAcceptClients listener
  }
Run Code Online (Sandbox Code Playgroud)

CancellationToken传递给x.AcceptWebSocketAsync取消时,返回null,然后AsyncAcceptWebSocket返回方法None.我可以通过断点验证这一点.

但是,AsyncAcceptClients(调用者),永远不会获得该None值,该方法刚刚结束,并且"Stop accepting clients.\n"永远不会显示在控制台上.如果我将所有内容包装在try\finally:

let rec AsyncAcceptClients(listener : WebSocketListener) =
  async {
    try
        let! result = listener.AsyncAcceptWebSocket
        match result with
            | None -> printf "Stop accepting clients.\n"
            | Some client ->
                Async.Start <| AsyncAcceptMessages client
                do! AsyncAcceptClients listener
   finally
        printf "This message is actually printed"
  }
Run Code Online (Sandbox Code Playgroud)

然后我在返回finally时放入执行的内容,但是我仍然没有执行的代码.(实际上,它会为每个连接的客户端在块上打印一次消息,所以也许我应该采用迭代方法?)listener.AsyncAcceptWebSocketNonematchfinally

但是,如果我使用自定义CancellationToken而不是Async.DefaultCancellationToken,一切都按预期工作,并"Stop accepting clients.\n"在屏幕上打印消息.

这里发生了什么?

Tom*_*cek 13

关于这个问题有两件事:

  • 首先,当在F#中发生取消时,AwaitTask不会返回null,而是该任务抛出OperationCanceledException异常.所以,你没有获得None价值,而是获得一个例外(然后F#也运行你的finally块).

    令人困惑的是,取消是一种特殊的异常,无法在async块内的用户代码中处理- 一旦你的计算被取消,它就不能被取消,它将永远停止(你可以进行清理finally).您可以解决此问题(请参阅此SO答案),但这可能会导致意外情况.

  • 其次,我不会使用默认取消令牌 - 这是所有异步工作流共享的,所以它可能会做出意想不到的事情.您可以使用Async.CancellationToken它来访问当前的取消令牌(F#会自动为您传播 - 因此您不必像在C#中那样手动传递它).

编辑:澄清了F#异步如何处理取消异常.