Why is the synchronization context null even when

2019-06-17 05:08发布

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";
            });
        }
    }
}
  1. Why is the synchronization context returning null in the anonymous method that I pass to Task.Run in FooAsync even when I try to set button1's Text 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.

  1. 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 sets button1's Text 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?

3条回答
一纸荒年 Trace。
2楼-- · 2019-06-17 05:40

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:

public async Task<string> FooAsync()
{
    // Will give you the sync ctx of the calling thread
    // which, in this case, happens to be the UI thread
    var ctx = SynchronizationContext.Current;

    return await Task.Run(() => 
    {
        if (ctx != null)
        {
            // Use ctx here

            // button1.Text = ctx.GetType().Name;
            Debugger.Break();
        }

        return "Hello";
    });
}

And not this, which is what I was doing:

public async Task<string> FooAsync()
{
    return await Task.Run(() => 
    {
        // Will get us the sync context of the worker,
        // which there is none, thus null
        var ctx = SynchronizationContext.Current;

        if (ctx != null)
        {
            // Use ctx here

            // button1.Text = ctx.GetType().Name;
            Debugger.Break();
        }

        return "Hello";
    });
}

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?

查看更多
一夜七次
3楼-- · 2019-06-17 05:45

Why is the synchronization context returning null in the anonymous method that I pass to Task.Run in FooAsync even when I try to set button1's Text property?

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.

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 set's button1's Text property, trying to post something to the Windows message pump. Is that correct? And if it is, why so?

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 and await in FooAsync(), 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 the Task.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:

private async void button1_Click(object sender, EventArgs e)
{
    var t = FooAsync();

    ...

    var s = await t;

    button1.Text = s;
}

public async Task<string> FooAsync()
{
    var something = await Task.Run(() => DoCPUIntensiveNonUIStuff());

   DoSomeUIWork();

   return ...
}

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.

查看更多
何必那么认真
4楼-- · 2019-06-17 05:59

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 what Progress 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 the Task.Run(...) with a Task.Delay(10) and see the same problem. It is the await 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:

public async Task<string> FooAsync()
{
    Task<String> task = Task.Run(() => 
    {
        //...
    });

    string result = await task;

    return result;
}

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)

public async Task<string> FooAsync()
{
    Task<String> task = Task.Run(() => 
    {
        //...
    });

    string result = await task.ConfigureAwait(false);

    return result;
}
查看更多
登录 后发表回答