【问题标题】:F# How Async<'T> cancellation works?F# Async<'T> 取消如何工作?
【发布时间】:2014-11-19 16:08:38
【问题描述】:

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

例如,我的这段代码依赖于使用 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" 消息会打印在屏幕上。

这是怎么回事?

【问题讨论】:

    标签: f# f#-3.0 cancellation f#-async


    【解决方案1】:

    这个问题有两点:

    • 首先,当 F# 中发生取消时,AwaitTask 不会返回 null,而是会引发 OperationCanceledException 异常。因此,您不会取回 None 值,而是会得到一个异常(然后 F# 也会运行您的 finally 块)。

      令人困惑的是,取消是一种特殊类型的异常,无法在 async 块内的用户代码中处理 - 一旦您的计算被取消,它就无法取消并且它总是会停止(你可以这样做清理finally)。您可以解决此问题(请参阅this SO answer),但它可能会导致意想不到的事情。

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

    编辑:阐明了 F# 异步如何处理取消异常。

    【讨论】:

    • 关于你的第一点,也许我没有正确解释自己。 AcceptWebSocketAsync 是在接受 CancellationToken 的 C# 库中声明的方法。当触发取消时,该方法返回null 而不是WebSocket。这是专门设计的。
    • @vtortola 我澄清了 F# 如何处理这个问题 - 但基本上,一旦令牌被取消,F# async 将不允许您继续运行异步工作流程(它不能取消)。因此,如果您真的想继续工作流程,我会为该方法创建一个单独的CancellationTokenSource(而不是将其传递给Async.Start
    • 我明白了,谢谢。我还在纠结这个。
    • 写答案时我也有点困惑:-)。但我认为关键点(一旦工作流程被取消,就不能取消)很有意义。因此,您需要对取消令牌进行更高级的管理。
    • 好吧,是的,如果您创建一个自定义令牌并将其传递给Async.Start,那么工作流将被取消(当令牌被取消时)并且只有finally 块将在清理期间运行。因此,您需要一个单独的令牌用于该方法(如果您不想取消工作流程,请不要将其传递给 Async.Start)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-31
    • 1970-01-01
    • 2020-04-07
    • 1970-01-01
    • 2017-12-03
    • 2011-05-23
    相关资源
    最近更新 更多