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

Posted

技术标签:

【中文标题】在用户界面和控制台应用程序中使用 Task.Yield() 的区别【英文标题】:Difference between using Task.Yield() in a User Interface and Console App 【发布时间】: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($"Runningnew 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 工作)。尝试以与此答案中所示类似的方式实例化和运行您的主表单:***.com/a/277776/2819245。尝试一下,看看它是否能改善(或改变)行为。 另外,由于您使用任务(因此可能使用多个线程),请注意您在程序的主线程中创建消息泵(即执行Application.Run)而不是在某个后台线程中。 正如@elgonzo 指出的那样,您链接的代码(我是其作者)旨在从带有消息循环的 WinForms UI 线程运行。你说 " "但是当我从另一个人的外面打电话时它不起作用。这是我的控制台应用程序...” 那么,您最终需要从另一个表单还是从非 UI 工作线程运行它? @noseratio 我不需要它,我更想知道为什么它适用于 WinForms 而不适用于控制台应用程序。 【参考方案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 线程。

【讨论】:

以上是关于在用户界面和控制台应用程序中使用 Task.Yield() 的区别的主要内容,如果未能解决你的问题,请参考以下文章

在 MFP 推送通知中注册用户 ID 和显示名称

如何在 NestJS 控制器处理程序的请求中获取“已验证的正文”和“已验证的用户”?

如何使用 CoreData 控制用户输入

005_控制器和动作

文件应用程序视图控制器?

组合标签栏和导航栏控制器出现问题