【问题标题】:Difference between using Task.Yield() in a User Interface and Console App在用户界面和控制台应用程序中使用 Task.Yield() 的区别
【发布时间】:2021-04-21 08:59:03
【问题描述】:

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

如下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)

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

【问题讨论】:

  • 我相信您的任务不会自己挂起,而是您的问题是由于尝试在控制台应用程序中建立基于 WinForms 的 UI 而没有设置消息泵(这是WinForms UI 工作)。尝试以与此答案中所示类似的方式实例化和运行您的主表单:stackoverflow.com/a/277776/2819245。尝试一下,看看它是否能改善(或改变)行为。
  • 另外,由于您使用任务(因此可能使用多个线程),请注意您在程序的主线程中创建消息泵(即执行Application.Run)而不是在某个后台线程中。
  • 正如@elgonzo 指出的那样,您链接的代码(我是其作者)旨在从带有消息循环的 WinForms UI 线程运行。你说 " "但是当我从另一个人的外面打电话时它不起作用。这是我的控制台应用程序...” 那么,您最终需要从另一个表单还是从非 UI 工作线程运行它?
  • @noseratio 我不需要它,我更想知道为什么它适用于 WinForms 而不适用于控制台应用程序。

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


【解决方案1】:

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

为什么会这样?

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

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

我更感兴趣的是了解为什么它适用于 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 async 事件处理程序在该同步上下文中调用,因此其中的逻辑应该按预期工作。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-21
    • 1970-01-01
    • 1970-01-01
    • 2014-01-02
    相关资源
    最近更新 更多