首页 > 解决方案 > 多个 Winforms 项目中的异步等待

问题描述

当线程在后台运行时,我们试图在 UI 中获取状态更新。下面的代码应该允许它,但实际上我们只有在所有线程完成后才能获得更新,而不是在它们运行时。与串行运行任务相比,我们也没有看到显着的性能改进,因此我们可能在这里做错了

该解决方案包括两个带有 winForm 的项目,第一个调用第二个。WinClient 命名空间用于 Winform 客户端。它调用 Services.modMain:

namespace WinClient
{
    static class Program
    {

        [STAThread]
        static void Main()
        {
            //call another winform project and wait for it to complete
            Services.modMain.loadObjects().Wait();
            //run local form
            Application.Run(new Form1());
        }
    }
}

Service.modMain 是应用程序不断获取数据并在内存中更新数据的地方。当它这样做时,它会将状态消息写入始终保持打开状态的启动表单。一旦 Service.modMain 完成初始数据加载,Form1(本例中为空表单)应该打开,而 splashForm 也保持打开状态

namespace Services
{
    public static class modMain
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        public static void Main()
        {

        }

        public static async Task loadObjects()
        {
            frmSplash.DefInstance.LoadMe();

            Progress<PrintToSplashMessage> messageToWindow = new Progress<PrintToSplashMessage>();
            messageToWindow.ProgressChanged += reportProgress;

            frmSplash.DefInstance.print_to_window("Starting Services", Color.Black, true);

            Task<bool> load1Task = load1(messageToWindow);
            Task<bool> load2Task = load2(messageToWindow);
            await Task.WhenAll(load1Task, load2Task);

        }

        private static async Task<bool> load2(IProgress<PrintToSplashMessage> progress)
        {
            return await Task<bool>.Run(() =>
            {
                PrintToSplashMessage theMessage = new PrintToSplashMessage("Load2, please wait...", Color.Black, true, false);
                progress.Report(theMessage);
                for (int i = 0; i != 100; ++i)
                {
                    Thread.Sleep(100); // CPU-bound work
                }
                return true;
            });
        }

        private static async Task<bool> load1(IProgress<PrintToSplashMessage> progress)
        {
            return await Task<bool>.Run(() =>
            {
                PrintToSplashMessage theMessage = new PrintToSplashMessage("Load1, please wait...", Color.Black, true, false);
                progress.Report(theMessage);
                for (int i = 0; i != 100; ++i)
                {
                    Thread.Sleep(100); // CPU-bound work
                }
                return true;
            });
        }

        private static void reportProgress(object sender, PrintToSplashMessage e)
        {
            frmSplash.DefInstance.PrintToSplashWindow(e);
        }
    }
}

PrintToSplashWindow 只是一个用于存储进度数据的实用程序类:

namespace Services
{
    public class PrintToSplashMessage
    {
        public string Message { get; set; }
        public Color MessageColor { get; set; }
        public bool OnNewLine { get; set; }
        public bool PrintToLog { get; set; }

        public PrintToSplashMessage(String theMessage, Color theMessageColor, bool isOnNewLine, bool needPrintToLog)
        {
            Message = theMessage;
            MessageColor = theMessageColor;
            OnNewLine = isOnNewLine;
            PrintToLog = needPrintToLog;
        }
    }
}

最后,这里是 frmSplash:

namespace Services
{
    public partial class frmSplash : Form
    {

        public frmSplash() :base()
        {
            InitializeComponent();
        }



        public void PrintToSplashWindow(PrintToSplashMessage theMessage)
        {
            print_to_window(theMessage.Message, theMessage.MessageColor, theMessage.OnNewLine);
        }

        public void print_to_window(string strShortMsg, Color lngColor, bool blnOnNewLine)
        {
            string strNewLine = String.Empty;
            if (blnOnNewLine)
            {
                if ( rtbErrorDisplay.Text.Length > 0)
                {
                    strNewLine = Environment.NewLine;
                }
                else
                {
                    strNewLine = "";
                }
            }
            else
            {
                strNewLine = "";
            }
            rtbErrorDisplay.SelectionStart = rtbErrorDisplay.Text.Length;
            rtbErrorDisplay.SelectionColor = lngColor;
            rtbErrorDisplay.SelectedText = strNewLine + strShortMsg;
            rtbErrorDisplay.SelectionStart = rtbErrorDisplay.Text.Length;
            rtbErrorDisplay.ScrollToCaret();

            Application.DoEvents();
        }
    }
}

我们期望的是,当任务在后台运行时,frmSplash 会显示进度消息。在实践中,它只在一切都完成后才批量显示。

标签: c#winformsasync-await

解决方案


简短版本:在您发布的代码中处理窗口消息的唯一方法是调用Application.DoEvents(). 但是代码可能永远不会走那么远,或者如果确实如此,则调用发生在错误的线程上。

更长的版本:

您没有包含实际的MCVE,所以我没有费心去测试,但是Progress该类依赖于同步上下文来工作。由于您还没有调用Application.Run(),因此可能根本没有同步上下文。在这种情况下Progress,只会使用线程池来调用订阅它的任何处理程序。

这意味着当您调用 时Application.DoEvents(),您处于线程池线程中,而不是拥有您的启动窗口的线程。

Windows 由线程拥有,它们的消息进入该线程的消息队列。该Application.DoEvents()方法将为当前线程的消息队列检索消息,但不为其他线程的队列处理消息。

在最坏的情况下,该线程有一个同步上下文(我不记得了……有可能因为线程是 STA,框架已经为你创建了一个),但是由于你没有消息循环,所以没有任何东西排队永远被派遣。进度报告只是不断堆积,从未处理过。

你应该完全放弃Application.DoEvents()。跟注DoEvents()总是麻烦,有更好的选择。

在这种情况下,Application.Run()也可用于第一种形式(初始屏幕)。创建该表单并订阅其FormShown事件,以便您知道何时调用loadObjects()。在该方法结束时,关闭表单,这样Application.Run()将返回并继续下一次Application.Run()调用。

这是基于您发布的代码的示例,我填写了详细信息(对于这两种形式,只需使用设计器创建一个默认Form对象……其余的初始化在下面的用户代码中)。

对于闪屏类,我推断出大部分内容,其余部分直接从您的代码中获取。我对您的代码所做的唯一更改是删除对以下的调用Application.DoEvents()

partial class SplashScreen : Form
{
    public static SplashScreen Instance { get; } = new SplashScreen();

    private readonly RichTextBox richTextBox1 = new RichTextBox();

    public SplashScreen()
    {
        InitializeComponent();

        // 
        // richTextBox1
        // 
        richTextBox1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
        richTextBox1.Location = new Point(13, 13);
        richTextBox1.Name = "richTextBox1";
        richTextBox1.Size = new Size(775, 425);
        richTextBox1.TabIndex = 0;
        richTextBox1.Text = "";
        Controls.Add(richTextBox1);
    }

    public void PrintToSplashWindow(PrintToSplashMessage theMessage)
    {
        print_to_window(theMessage.Message, theMessage.MessageColor, theMessage.OnNewLine);
    }

    public void print_to_window(string strShortMsg, Color lngColor, bool blnOnNewLine)
    {
        string strNewLine = String.Empty;
        if (blnOnNewLine)
        {
            if (richTextBox1.Text.Length > 0)
            {
                strNewLine = Environment.NewLine;
            }
            else
            {
                strNewLine = "";
            }
        }
        else
        {
            strNewLine = "";
        }
        richTextBox1.SelectionStart = richTextBox1.Text.Length;
        richTextBox1.SelectionColor = lngColor;
        richTextBox1.SelectedText = strNewLine + strShortMsg;
        richTextBox1.SelectionStart = richTextBox1.Text.Length;
        richTextBox1.ScrollToCaret();
    }
}

我不清楚为什么你有两个不同的类,这两个类似乎都被设置为程序的入口点。我将它们合并为一个类:

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        loadObjects();
        Application.Run(new Form1());
    }

    public static void loadObjects()
    {
        SplashScreen.Instance.Shown += async (sender, e) =>
        {
            Progress<PrintToSplashMessage> messageToWindow = new Progress<PrintToSplashMessage>();
            messageToWindow.ProgressChanged += reportProgress;

            SplashScreen.Instance.print_to_window("Starting Services", Color.Black, true);

            Task<bool> load1Task = load1(messageToWindow);
            Task<bool> load2Task = load2(messageToWindow);
            await Task.WhenAll(load1Task, load2Task);
            SplashScreen.Instance.Close();
        };

        SplashScreen.Instance.ShowDialog();
    }

    private static async Task<bool> load2(IProgress<PrintToSplashMessage> progress)
    {
        return await Task.Run(() =>
        {
            PrintToSplashMessage theMessage = new PrintToSplashMessage("Load2, please wait...", Color.Black, true, false);
            progress.Report(theMessage);
            for (int i = 0; i < 10; ++i)
            {
                Thread.Sleep(TimeSpan.FromSeconds(1)); // CPU-bound work
                theMessage.Message = $"Load2, i = {i}";
                progress.Report(theMessage);
            }
            return true;
        });
    }

    private static async Task<bool> load1(IProgress<PrintToSplashMessage> progress)
    {
        return await Task.Run(() =>
        {
            PrintToSplashMessage theMessage = new PrintToSplashMessage("Load1, please wait...", Color.Black, true, false);
            progress.Report(theMessage);
            for (int i = 0; i < 10; ++i)
            {
                Thread.Sleep(TimeSpan.FromSeconds(1)); // CPU-bound work
                theMessage.Message = $"Load1, i = {i}";
                progress.Report(theMessage);
            }
            return true;
        });
    }

    private static void reportProgress(object sender, PrintToSplashMessage e)
    {
        SplashScreen.Instance.PrintToSplashWindow(e);
    }
}


推荐阅读