A canceled task does not return control to an asynchronous block

I tried to reduce this to the smallest possible reproduction, but it is still a little long, my apologies.

I have a F # project that references a C # project with code like the following.

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

Here's the code 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() 

The problem is that after cancellation, control never returns to the asynchronous block. I'm not sure if it hangs in AwaitTask or Catch . Intuition tells me that it blocks when I try to return to the previous synchronization context, but I'm not sure how to confirm this. I am looking for ideas on how to fix this problem, or maybe someone with a deeper understanding here can identify the problem.

POSSIBLE SOLUTION

 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 
+7
asynchronous f # task cancellation mailboxprocessor
source share
1 answer

Ultimately, this makes annulment in F # Async special. Cancellation effectively translates into stop and teardown . As you can see in source , undoing in Task does everything possible outside of computing.

If you need the good old OperationCanceledException , which you can handle as part of your calculation, we can just make your own.

 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 ) 

Cancellation is now another exception - and exceptions that we can handle. Here's the reproduction:

 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 
+3
source share

All Articles