我对使用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
}
取消传递给CancellationToken
的{{1}}后,返回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"
}
返回finally
时,listener.AsyncAcceptWebSocket
中的内容会被执行,但None
中的代码仍然没有。 (实际上,它会为每个连接的客户端在match
块上打印一次消息,所以也许我应该采用迭代方法?)
但是,如果我使用自定义finally
而不是CancellationToken
,则所有内容都会按预期工作,并且Async.DefaultCancellationToken
消息会在屏幕上显示。
这里发生了什么?
答案 0 :(得分:12)
这个问题有两个方面:
首先,当F#中发生取消时,AwaitTask
不会返回null
,而是会导致OperationCanceledException
异常。因此,您不会获得None
值,而是获得异常(然后F#也会运行您的finally
块。)
令人困惑的是,取消是一种特殊的异常,无法在async
块内的用户代码中处理 - 一旦您的计算被取消,它就不会被取消而且它将永远停止(您可以在finally
)进行清理。您可以解决此问题(请参阅this SO answer),但这可能会导致意外情况。
其次,我不会使用默认取消令牌 - 这是所有异步工作流共享的,所以它可能会做出意想不到的事情。您可以使用Async.CancellationToken
来访问当前的取消令牌(F#会自动为您传播 - 因此您不必像在C#中那样手动传递它)。
编辑:阐明F#异步如何处理取消例外。