Reputation: 2312
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
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