1

I am looking into a problem where a chain of method calls between various C# async methods and F# async functions is hanging a program. I think the F# script below reproduces the same problem by mixing F# async and TPL:

open System
open System.Threading
open System.Threading.Tasks

let testy = async {
    let tokenSource = new CancellationTokenSource();

    Task.Run(fun () ->
        printfn "Inside first task"
        Thread.Sleep(1000)
        printfn "First task now cancelling the second task"
        tokenSource.Cancel()) |> ignore

    let! result =
        Task.Factory.StartNew<int>((fun () ->
            printfn "Inside second task"
            Thread.Sleep(2000)
            printfn "Second task about to throw task cancelled exception"
            tokenSource.Token.ThrowIfCancellationRequested()
            printfn "This is never reached, as expected"
            0),
            tokenSource.Token)
        |> Async.AwaitTask

    return result }

printfn "Starting"
let result = testy |> Async.StartAsTask |> Async.AwaitTask |> Async.RunSynchronously
printfn "Program ended with result: %d" result

Running this program in FSI hangs the interpreter. I get the following output before it hangs:

Starting
Inside first task
Inside second task
First task now cancelling the second task
Second task about to throw task cancelled exception

I've discovered that if I change the line

let result = testy |> Async.StartAsTask |> Async.AwaitTask |> Async.RunSynchronously

to

let result = testy |> Async.RunSynchronously

then it no longer hangs and "OperationCanceledException" is shown in the FSI as expected, but I don't know why.

junichiro
  • 5,282
  • 3
  • 18
  • 26

1 Answers1

4

The cancellation token is being passed to Task.Factory.StartNew. So when it's canceled Async.StartAsTask will never start and always report a status of WaitingForActivation. If the token isn't passed to Task.Factory.StartNew then the status will change to 'Faulted' which will unblock Async.AwaitTask which will allow Async.RunSynchronously to rethrow the exception.

To fix

let result = testy |> Async.StartAsTask |> Async.AwaitTask |> Async.RunSynchronously

The same cancellation token needs to be passed to Async.StartAsTask.

let result = 
    Async.StartAsTask (testy, TaskCreationOptions.None, tokenSource.Token) 
    |> Async.AwaitTask 
    |> Async.RunSynchronously
gradbot
  • 13,732
  • 5
  • 36
  • 69
  • Thanks! Unfortunately in the real code the task cancellation is caused by an HttpClient timeout, and in that case passing the cancellation token to both HttpClient.SendAsync and Async.StartAsTask doesn't fix the issue. I wonder whether maybe this is due to http://stackoverflow.com/questions/29319086/cancelling-an-httpclient-request-why-is-taskcanceledexception-cancellationtoke, it sounds like HttpClient does weird stuff with cancellation tokens – junichiro Dec 14 '15 at 08:49