取消的任务不会返回控制到异步块

7

我试图将这个问题简化到最小的可重现状态,但它仍然比较长,抱歉。

我有一个F#项目,引用了一个包含以下代码的C#项目。

public static class CSharpClass {
    public static async Task AsyncMethod(CancellationToken cancellationToken) {
        await Task.Delay(3000);
        cancellationToken.ThrowIfCancellationRequested();
    }
}

下面是F#代码。

type Message = 
    | Work of CancellationToken
    | Quit of AsyncReplyChannel<unit>

let mkAgent() = MailboxProcessor.Start <| fun inbox -> 
    let rec loop() = async {
        let! msg = inbox.TryReceive(250)
        match msg with
        | Some (Work cancellationToken) ->
            let! result = 
                CSharpClass.AsyncMethod(cancellationToken)
                |> Async.AwaitTask
                |> Async.Catch
            // THIS POINT IS NEVER REACHED AFTER CANCELLATION
            match result with
            | Choice1Of2 _ -> printfn "Success"
            | Choice2Of2 exn -> printfn "Error: %A" exn    
            return! loop()
        | Some (Quit replyChannel) -> replyChannel.Reply()
        | None -> return! loop()
    }
    loop()

[<EntryPoint>]
let main argv = 
    let agent = mkAgent()
    use cts = new CancellationTokenSource()
    agent.Post(Work cts.Token)
    printfn "Press any to cancel."
    System.Console.Read() |> ignore
    cts.Cancel()
    printfn "Cancelled."
    agent.PostAndReply Quit
    printfn "Done."
    System.Console.Read()

问题在于,一旦取消操作,控制权永远不会返回到异步块。我不确定它是否在AwaitTaskCatch中挂起。直觉告诉我,在尝试返回到先前的同步上下文时会阻塞,但我不确定如何确认这一点。我正在寻找解决此问题的方法,或者也许有更深入了解的人可以发现问题。
可能的解决方案:
let! result = 
    Async.FromContinuations(fun (cont, econt, _) ->
        let ccont e = econt e
        let work = CSharpClass.AsyncMethod(cancellationToken) |> Async.AwaitTask
        Async.StartWithContinuations(work, cont, econt, ccont))
    |> Async.Catch

1
OperationCanceledException在F#中有特殊处理,总是会停止整个异步过程,一直到最上层。请参考这个答案:https://dev59.com/ll8d5IYBdhLWcg3wQwa5#27022315 - Fyodor Soikin
即使向 C# 方法传递了唯一的取消令牌,并且没有被任何 F# 异步代码共享,这仍然是正确的吗? - Daniel
是的。这不是令牌的问题,而是OperationCanceledException的处理方式。你甚至可以自己抛出它,而不涉及令牌,它仍然会停止整个异步操作。 - Fyodor Soikin
我更新了我的问题,并从您提供的答案中获得了一个解决方案,它似乎有效。这是捕获取消异常的理想方式吗? - Daniel
不知道,这取决于你对“理想”的定义。我真的不能告诉你比那些答案中描述的更多了。 - Fyodor Soikin
显示剩余5条评论
1个回答

3
最终导致这种行为的原因是取消在 F# Async 中很特殊。取消实际上相当于 停止和撤销。正如您可以在 源代码 中看到的那样,在 Task 中取消操作一直持续到计算结束。
如果您想要好的旧版 OperationCanceledException,并且希望将其作为计算的一部分处理,我们可以自己制作它。
type Async =
    static member AwaitTaskWithCancellations (task: Task<_>) =
        Async.FromContinuations(fun (setResult, setException, setCancelation) ->
            task.ContinueWith(fun (t:Task<_>) -> 
                match t.Status with 
                | TaskStatus.RanToCompletion -> setResult t.Result
                | TaskStatus.Faulted -> setException t.Exception
                | TaskStatus.Canceled -> setException <| OperationCanceledException()
                | _ -> ()
            ) |> ignore
        )

取消现在只是另一个异常 - 我们可以处理异常。以下是复制的内容:
let tcs = TaskCompletionSource<unit>()
tcs.SetCanceled()

async { 
    try        
        let! result = tcs.Task |> Async.AwaitTaskWithCancellations
        return result
    with
         | :? OperationCanceledException -> 
           printfn "cancelled"      
         | ex -> printfn "faulted %A" ex

    ()
} |> Async.RunSynchronously

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