I have a button on a form in the click of which I call FooAsync
and block the UI thread on its completion.
Below is the code and my questions.
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SynContextIfIDontTouchUIInWorkerThread
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
#pragma warning disable 1998
private async void button1_Click(object sender, EventArgs e)
{
// Nicely prints out the WindowsForms.SynchronizationContext
// because we *are* indeed on the UI thread
this.Text = SynchronizationContext.Current.GetType().Name;
Thread.CurrentThread.Name = "UI Thread";
Debug.Print(Thread.CurrentThread.Name);
var t = FooAsync();
// CompletedSynchronously is false,
// so the other work was indeed run on a worker thread
button1.Text = (t as IAsyncResult).CompletedSynchronously
? "Sync" : "Async";
// block the UI thread
// Code freezes here
var s = t.Result;
button1.Text = s;
}
#pragma warning restore 1998
public async Task<string> FooAsync()
{
return await Task.Run(() =>
{
// Whether or not I touch the UI in this worker
// thread, the current sync context returns null.
// Why is that?
// However, it looks like this thread is posting
// something to the UI thread and since the UI
// thread is also waiting for this guy to complete
// it results in a dead lock. Why is that when
// I am not even touching the UI here. Why
// is this guy assuming that I have to post
// something to message queue to run on the UI thread?
// Could it be that this guy is actually running on
// the UI thread?
var ctx = SynchronizationContext.Current;
Debugger.Break();
// Current thread name evaluates to null
// This clearly means it is a thread pool thread
// Then why is the synchronization context null
// when I uncomment out the line that changes the text
// of button1?
Debug.Print(Thread.CurrentThread.Name);
if (ctx != null)
{
// Post to Windows message queue using the UI thread's sync ctx
// button1.Text = ctx.GetType().Name;
Debugger.Break();
}
return "Hello";
});
}
}
}
- Why is the synchronization context returning
null
in the anonymous method that I pass toTask.Run
inFooAsync
even when I try to setbutton1
'sText
property?
And the synchronization context is null
if I don't do anything to the UI from within the anonymous method, which is the behavior I expected from my present understanding of a synchronization context.
- Why is there this dead-lock? It looks like the anonymous method passed to
Task.Run
, even though clearly running on a thread pool thread and even when not touching the UI, i.e. when I comment out the line that setsbutton1
'sText
property, is trying to post something to the Windows message pump. Is that correct? And if it is, why so?
At any rate, what is happening? What is causing the dead lock. I understand that the UI thread is blocked, but why is this other worker thread trying to wait on the UI thread to be free?
Ah, classic mistake I made. I got it.
When you call
SynchronizationContext.Current
, it gives you the synchronization context of the current thread it is running on.What I should have done, and which is also what the awaiter does when it builds a continuation by exploding the
await
into a state machine is this:And not this, which is what I was doing:
This answers the first of my two questions.
The second one still remains. Why is it trying to post to the UI thread?
Oh yes, may be, and may be because it sees that the current synchronization context is not null, it tries to acquire that context and perform the work within that context, without any regard to what the body of the method is doing. Is that right?
Inside the anonymous method, the code runs in a thread-pool thread. It is normal in this case for the synchronization context to be null. In the normal cases, you should expect the synchronization context in UI applications to be non-null when you are running within the UI thread only.
If you try to change the value of
button1.Text
inside the anonymous method, you will get an exception because only the UI thread can update the UI. .NET does not do any magic in this case to use the UI thread to update the UI.Because
await Task.Run(() ...
is scheduling a continuation on the UI thread, and since you are using the UI thread to synchronously wait for the task (via.Result
), then there is a deadlock. In other words, the continuation cannot proceed because the UI thread is busy waiting for the task.If you remove the
async
andawait
inFooAsync()
, you will get rid of the deadlock because no continuations on the UI thread will be attempted.Another way to remove the dead lock is to tell the
await
for theTask.Run...
not to capture the synchronization context by calling.ConfigureAwait(false);
on the task.In any case, I think that you are probably not doing things in the correct way.
You probably should be doing something like this:
In this case, the async/await magic will work (capturing the SynchronizationContext and then using it when continuing), and the
DoSomeUIWork()
method will run using the UI thread.Take a look at this article about async/await.
1) The
Task.Run
does not set the synchronization context when it starts the thread pool thread. Once you lose the context you can not "get it back" unless you explicitly made a copy of it before it was lost. This, for example, is whatProgress
does to make the invocations run on the correct thread. It gets a copy of the current synchronization context in the constructor.2) The body of the
Task.Run
has nothing to do with your deadlock, you could replace theTask.Run(...)
with aTask.Delay(10)
and see the same problem. It is theawait
you do outside of it that causes the problem. Let me re-write your function slightly to break apart the 3 steps that are happening:The way you wrote your code you told the system to run the line
return result;
on the synchronization context if it is available (which it is), however you are blocking thread the synchronization context with the.Result
and the.Result
won't unblock until the function returns a value. A deadlock.One way around this is tell the
return result;
to not necessarily use the synchronization context even it is available by telling it.ConfigureAwait(false)