F#中的Async<'T>取消是如何工作的?

13

我对在C#中使用TPL进行异步取消操作非常熟悉,但是我对在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
  }

当传递给x.AcceptWebSocketAsyncCancellationToken被取消时,返回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"
  }

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

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

这里到底发生了什么?

1个回答

16

这个问题有两个注意点:

  • 首先,在 F# 中发生取消操作时,AwaitTask 不会返回 null,而是抛出 OperationCanceledException 异常。因此,你不会得到 None 值,而是得到一个异常(然后 F# 也会运行你的 finally 代码块)。

    令人困惑的是,取消是一种特殊的异常,无法在 async 块内的用户代码中处理 - 一旦你的计算被取消,它将永远停止(你可以在 finally 中进行清理)。你可以解决这个问题(参见这个 SO 回答),但可能会引起意外情况。

  • 其次,我不会使用默认取消标记 - 这个标记是所有异步工作流共享的,因此可能会产生意外情况。相反,你可以使用 Async.CancellationToken,它可以让你访问当前的取消标记(F# 会自动为你传播它 - 所以你不必像在 C# 中那样手动传递它)。

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


关于你的第一点,也许我表述得不够清楚。AcceptWebSocketAsync是一个在C#库中声明的方法,它接受一个CancellationToken。当取消操作被触发时,该方法返回null而不是一个WebSocket。这种设计是特意为之。 - vtortola
1
@vtortola 我解释了 F# 如何处理这个问题 - 基本上,一旦令牌被取消,F# 异步将不会让您继续运行异步工作流程(它无法被取消)。因此,如果您真的想要继续工作流程,我建议为该方法创建一个单独的 CancellationTokenSource(而不是将其传递给 Async.Start)。 - Tomas Petricek
我明白了,谢谢你。我还在努力理解这个。 - vtortola
1
写答案时我也有点困惑 :-). 但是我认为关键点(一旦工作流被取消,它就无法被取消)很有道理。因此,您需要更高级的取消标记管理。 - Tomas Petricek
1
如果您创建了一个自定义令牌并将其传递给 Async.Start,那么当该令牌被取消时,工作流程将被取消,并且在清理期间只有 finally 块将运行。因此,您需要为该方法使用单独的令牌(如果不想取消工作流程,则不要将其传递给 Async.Start)。 - Tomas Petricek
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接