在用户界面和控制台应用程序中使用Task.Yield()之间的区别

时间:2018-12-20 18:53:58

标签: c# .net winforms async-await console

我正在尝试异步显示一个进度表,其中说应用程序正在运行,而实际应用程序正在运行。

如下this question所示,我有以下内容:

主表单:

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
    }

    async Task<int> LoadDataAsync()
    {
        await Task.Delay(2000);
        return 42;
    }

    private async void Run_Click(object sender, EventArgs e)
    {
        var runningForm = new RunningForm();

        runningForm.ShowRunning();

        var progressFormTask = runningForm.ShowDialogAsync();

        var data = await LoadDataAsync();

        runningForm.Close();
        await progressFormTask;

        MessageBox.Show(data.ToString());
    }
}

进度表

public partial class RunningForm : Form
{
    private readonly SynchronizationContext synchronizationContext;

    public RunningForm()
    {
        InitializeComponent();
        synchronizationContext = SynchronizationContext.Current;
    }

    public async void ShowRunning()
    {
        this.RunningLabel.Text = "Running";
        int dots = 0;

        await Task.Run(() =>
        {
            while (true)
            {
                UpadateUi($"Running{new string('.', dots)}");

                Thread.Sleep(300);

                dots = (dots == 3) ? 0 : dots + 1;
            }
        });
    }

    public void UpadateUi(string text)
    {
        synchronizationContext.Post(
            new SendOrPostCallback(o =>
            {
                this.RunningLabel.Text = text;
            }),
            text);
    }

    public void CloseThread()
    {
        synchronizationContext.Post(
            new SendOrPostCallback(o =>
            {
                this.Close();
            }),
            null);
    }
}

internal static class DialogExt
{
    public static async Task<DialogResult> ShowDialogAsync(this Form form)
    {
        await Task.Yield();
        if (form.IsDisposed)
        {
            return DialogResult.OK;
        }
        return form.ShowDialog();
    }
}

上面的方法工作正常,但是当我从另一个外部进行呼叫时却无法正常工作。这是我的控制台应用程序:

class Program
{
    static void Main(string[] args)
    {
        new Test().Run();
        Console.ReadLine();
    }
}

class Test
{
    private RunningForm runningForm;

    public async void Run()
    {
        var runningForm = new RunningForm();

        runningForm.ShowRunning();

        var progressFormTask = runningForm.ShowDialogAsync();

        var data = await LoadDataAsync();

        runningForm.CloseThread();

        await progressFormTask;

        MessageBox.Show(data.ToString());
    }

    async Task<int> LoadDataAsync()
    {
        await Task.Delay(2000);
        return 42;
    }
}

看着调试器发生了什么,该过程进入await Task.Yield(),并且从未进行到return form.ShowDialog(),因此您从未见过RunningForm。然后,该过程转到LoadDataAsync(),并永久挂在await Task.Delay(2000)上。

为什么会这样?与Task的优先级(即:Task.Yield())的优先级有关吗?

1 个答案:

答案 0 :(得分:2)

  

观看调试器发生的情况,等待该过程   Task.Yield()永远不会返回form.ShowDialog(),因此   您永远不会看到RunningForm。然后该过程转到   LoadDataAsync()并永久挂起,等待Task.Delay(2000)。

     

为什么会这样?

这里发生的是,当您在没有任何同步上下文的控制台线程上执行var runningForm = new RunningForm()时(System.Threading.SynchronizationContext.Current为null),它将隐式创建WindowsFormsSynchronizationContext的实例并将其安装在当前线程,有关此here的更多信息。

然后,当您按下await Task.Yield()时,ShowDialogAsync方法将返回给调用者,并且await延续将发布到该新的同步上下文中。但是,继续操作永远不会有被调用的机会,因为当前线程不会运行消息循环,发布的消息也不会被泵送。没有死锁,但是await Task.Yield()之后的代码从未执行,因此对话框甚至不会显示。 await Task.Delay(2000)也是如此。

  

我对了解为什么它适用于WinForms而不适用于WinForms更感兴趣   控制台应用程序。

您需要在控制台应用程序中使用带有消息循环的UI线程。尝试像这样重构您的控制台应用程序:

public void Run()
{
    var runningForm = new RunningForm();
    runningForm.Loaded += async delegate 
    {
        runningForm.ShowRunning();

        var progressFormTask = runningForm.ShowDialogAsync();

        var data = await LoadDataAsync();

        runningForm.Close();

        await progressFormTask;

        MessageBox.Show(data.ToString());
    };
    System.Windows.Forms.Application.Run(runningForm);
}

这里,Application.Run的工作是启动模式消息循环(并在当前线程上安装WindowsFormsSynchronizationContext),然后显示表单。 runningForm.Loaded异步事件处理程序是在该同步上下文上调用的,因此它内部的逻辑应按预期工作。

但是,这使Test.Run 是一种同步方法,即例如,仅在关闭表单且消息循环结束时才返回。如果这不是您想要的,则必须创建一个单独的线程来运行消息循环,就像我做的with MessageLoopApartment here

也就是说,在典型的WinForms或WPF应用程序中,您几乎永远不需要辅助UI线程。