Chechy Levas
Chechy Levas

Reputation: 2312

Why does my second snippet of F# async code work, but the first does not?

NB: I am not a professional software developer but I do write a lot of code that doesn't make use of asynchronous anything, so I apologize if this question is really straight forward.

I am interfacing with a library written in C#. There is a particular function (lets call it 'func') that returns a 'Threading.Tasks.Task>'

I am building a library in F# that makes use of func. I tested the following piece of code in a console app and it worked fine.

let result = 
        func()
        |> Async.AwaitTask
        |> Async.RunSynchronously
        |> Array.ofSeq

However, when I run it from a WinForms app (which is ultimately what I want to do), execution blocks in the form code and never returns.

So I messed around with the code and tried the following, which worked.

let result = 
        async{
            let! temp = 
                func()
                |> Async.AwaitTask

            return temp
        } |> Async.RunSynchronously |> Array.ofSeq

Why didn't the first snippet work? Why did the second snippet work? Is there anything on this page that would answer either of these questions? If so, it does not seem obvious. If not, can you point me to somewhere that does?

Upvotes: 10

Views: 501

Answers (1)

Fyodor Soikin
Fyodor Soikin

Reputation: 80915

The difference between your first and second snippets is that the AwaitTask call happens on different threads.

Try this out to verify:

let printThread() = printfn "%d" System.Threading.Thread.CurrentThread.ManagedThreadId

let result = 
    printThread()
    func()
    |> Async.AwaitTask
    |> Async.RunSynchronously
    |> Array.ofSeq

let res2 = 
    printThread()
    async {
        printThread()
        let! temp = func() |> Async.AwaitTask
        return temp
    } |> Async.RunSynchronously |> Array.ofSeq

When you run res2, you'll get two lines of output, with two different numbers on them. The thread on which the inside of async runs is not the same thread on which res2 itself runs. Diving into an async puts you on a different thread.

Now, this interacts with the way .NET TPL tasks actually work. When you go to await a task, you don't just get a callback on some random thread, oh no! Instead, your callback gets scheduled via the "current" SynchronizationContext. That's a special kind of beast, of which there is always one "current" one (accessible via a static property - talk about global state!), and you can ask it to schedule stuff "in the same context", where the concept of "same context" is defined by the implementation.

WinForms, of course, has its own implementation, aptly named WindowsFormsSynchronizationContext. When you're running within a WinForms event handler, and you ask the current context to schedule something, it will be scheduled using the WinForms' own event loop - a la Control.Invoke.

But of course, since you're blocking the event loop's thread with your Async.RunSynchronously, the task await never has a chance to happen. You're waiting for it to happen, and it is waiting for you to release the thread. Aka "deadlock".

To fix this, you need to start the awaiting on a different thread, so that WinForms' synchronization context isn't used - a solution on which you have accidentally stumbled.

An alternative recommended solution is to tell the TPL explicitly not to use the "current" context via Task.ConfigureAwait:

let result = 
    func().ConfigureAwait( continueOnCapturedContext = false )
    |> Async.AwaitTask
    |> Async.RunSynchronously
    |> Array.ofSeq

Unfortunately, this will not compile, because Async.AwaitTask expects a Task, and ConfigureAwait returns a ConfiguredTaskAwaitable.

Upvotes: 8

Related Questions