Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 569
Chechy Levas Avatar asked Jan 25 '18 17:01

Chechy Levas


People also ask

What is the F pattern of reading?

The F-Pattern describes the most common user eye-scanning patterns when it comes to blocks of content. 'F' means fast. That's how users read your content on the web. In a few seconds, users eyes move at amazing speeds across a' page.

How does the F-shaped reading pattern help?

Bearing the F-shaped pattern in mind will help you create a design with good visual hierarchy—a design that people can scan easily. The F-shaped layout feels comfortable for most western readers, because they have been used to reading from top to bottom / left to right, for their entire lives.

How do users read the web?

They don't. People rarely read Web pages word by word; instead, they scan the page, picking out individual words and sentences. In a recent study John Morkes and I found that 79 percent of our test users always scanned any new page they came across; only 16 percent read word-by-word.

Where do users look on a website?

Web users spend 69% of their time viewing the left half of a page and 30% of their time viewing the right half.


1 Answers

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.

like image 166
Fyodor Soikin Avatar answered Oct 17 '22 00:10

Fyodor Soikin