为什么即使我尝试从工作者更改UI,同步上下文也为null,为什么工作人员在UI线程上等待,即使我不这样做?

Wat*_* v2 7 .net c# asynchronous task-parallel-library async-await

我点击一个表单上有一个按钮,FooAsync并在完成时阻止UI线程.

以下是代码和我的问题.

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";
            });
        }
    }
}
Run Code Online (Sandbox Code Playgroud)
  1. 为什么同步上下文返回null在我传递给匿名方法Task.RunFooAsync,甚至当我尝试设置button1Text属性?

同步上下文是指null我不从匿名方法中对UI做任何事情,这是我对目前对同步上下文的理解所期望的行为.

  1. 为什么会出现这种死锁?它看起来像传递到匿名方法Task.Run,即使在一个线程池线程显然运行,即使不接触UI,即当我注释掉设置行button1Text性质,试图才能发布到Windows消息泵.那是对的吗?如果是,为什么呢?

无论如何,发生了什么?造成死锁的原因是什么.我知道UI线程被阻止,但为什么这个其他工作线程试图在UI线程上等待是免费的?

Yac*_*sad 6

为什么同步上下文在我传递给FooAsync中的Task.Run的匿名方法中返回null,即使我尝试设置button1的Text属性?

在匿名方法内部,代码在线程池线程中运行.在这种情况下,同步上下文为空是正常的.在正常情况下,当您仅在UI线程中运行时,您应该期望UI应用程序中的同步上下文为非null.

如果您尝试更改button1.Text匿名方法内部的值,则会出现异常,因为只有UI线程才能更新UI.在这种情况下,.NET不会使用UI线程来更新UI.

为什么会出现这种死锁?它看起来像传递到Task.Run匿名方法,即使在一个线程池线程显然运行,即使不接触UI,即当我注释掉设置的按钮1的Text属性行,试图才能发布到Windows消息泵.那是对的吗?如果是,为什么呢?

因为await Task.Run(() ...正在调度UI线程上的延续,并且由于您正在使用UI线程同步等待任务(via .Result),因此存在死锁.换句话说,由于UI线程忙于等待任务,因此无法继续.

如果删除asyncawaitin FooAsync(),则会消除死锁,因为不会尝试在UI线程上继续.

以去除死锁的另一种方式是告诉awaitTask.Run...没有通过调用来捕捉同步上下文.ConfigureAwait(false);的任务.

无论如何,我认为你可能没有以正确的方式做事.

你可能应该做这样的事情:

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 ...
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,async/await magic将起作用(捕获SynchronizationContext,然后在继续时使用它),并且该DoSomeUIWork()方法将使用UI线程运行.

看看这篇关于async/await的文章.


Sco*_*ain 2

1)Task.Run启动线程池线程时不设置同步上下文。一旦丢失上下文,您就无法“找回它”,除非您在丢失之前明确地复制了它。例如,这就是Progress使调用在正确的线程上运行的方法。它在构造函数中获取当前同步上下文的副本。

2) the 的正文Task.Run与你的死锁无关,你可以用Task.Run(...)a替换 theTask.Delay(10)并看到同样的问题。正是await您在其之外所做的事情导致了问题。让我稍微重写一下你的函数,以分解正在发生的 3 个步骤:

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

    string result = await task;

    return result;
}
Run Code Online (Sandbox Code Playgroud)

您编写代码的方式告诉系统在return result;同步上下文可用(确实如此)的情况下在同步上下文上运行该行,但是您正在使用 阻塞线程同步上下文,并且.Result.Result函数返回值之前不会解除阻塞。陷入僵局。

解决此问题的一种方法是告诉return result;不必使用同步上下文,即使它可用.ConfigureAwait(false)

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

    string result = await task.ConfigureAwait(false);

    return result;
}
Run Code Online (Sandbox Code Playgroud)