Отмененная задача не возвращает управление асинхронному блоку


Я попытался свести это к самому маленькому возможному упреку, но это все еще немного длинновато, мои извинения.

У меня есть проект 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()

Проблема заключается в том, что после отмены управление никогда не возвращается в асинхронный блок. Я не уверен, висит ли он в AwaitTask или Catch. Интуиция подсказывает мне, что он блокируется при попытке вернуться к предыдущему контексту синхронизации, но я не уверен, как это сделать. подтвердить это. Я ищу идеи о том, как устранить эту проблему, или, возможно, кто-то с более глубоким пониманием здесь может обнаружить проблему.

ВОЗМОЖНОЕ РЕШЕНИЕ

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 7

1 ответ:

Что в конечном счете вызывает такое поведение, так это то, что отмены являются особыми в 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