I tried to reduce this to the smallest possible repro, but it's still a bit long-ish, my apologies.
I have an 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 F# code.
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 issue is that, upon cancellation, control never returns to the async block. I'm not sure if it's hanging in AwaitTask
or Catch
. Intuition tells me it's blocking when trying to return to the previous sync context, but I'm not sure how to confirm this. I'm looking for ideas on how to troubleshoot this, or perhaps someone with a deeper understanding here can spot the issue.
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
You can cancel an asynchronous operation after a period of time by using the CancellationTokenSource. CancelAfter method if you don't want to wait for the operation to finish.
Async methods can have the following return types: Task, for an async method that performs an operation but returns no value. Task<TResult>, for an async method that returns a value. void , for an event handler.
TaskCanceledException(String, Exception, CancellationToken) Initializes a new instance of the TaskCanceledException class with a specified error message, a reference to the inner exception that is the cause of this exception, and the CancellationToken that triggered the cancellation. TaskCanceledException(Task)
NET, Task. Run is used to asynchronously execute CPU-bound code.
What ultimately causes this behavior is that cancellations are special in F# Async. Cancellations effectively translate to a stop and teardown. As you can see in the source, cancellation in the Task
makes it all the way out of the computation.
If you want the good old OperationCanceledException
which you can handle as part of your computation, we can just make our 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 just another exception - and exceptions, we can handle. Here's the repro:
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With