Multithreading Winforms更新具有高性能

Multithreading Winforms更新具有高性能,multithreading,winforms,task-parallel-library,backgroundworker,Multithreading,Winforms,Task Parallel Library,Backgroundworker,让我用一些背景信息来设置这个问题,我们有一个长时间运行的过程,它将以Windows窗体生成数据。因此,显然需要某种形式的多线程来保持表单的响应性。但是,我们还要求表单在保持响应性的同时每秒更新多次 下面是一个使用后台工作线程的简单测试示例: void bw_ProgressChanged(object sender, ProgressChangedEventArgs e) { int reportValue = (int)e.UserState; labe

让我用一些背景信息来设置这个问题,我们有一个长时间运行的过程,它将以Windows窗体生成数据。因此,显然需要某种形式的多线程来保持表单的响应性。但是,我们还要求表单在保持响应性的同时每秒更新多次

下面是一个使用后台工作线程的简单测试示例:

void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        int reportValue = (int)e.UserState;
        label1.Text = reportValue;
        //We can put this.Refresh() here to force repaint which gives us high repaints but we lose
        //all other responsiveness with the control

    }

    void bw_DoWork(object sender, DoWorkEventArgs e)
    {
            for (int x = 0; x < 100000; x++)
            {     
              //We could put Thread.Sleep here but we won't get highest performance updates
                bw.ReportProgress(0, x);                    
            }
    }
void bw\u ProgressChanged(对象发送方,ProgressChangedEventArgs e)
{
int reportValue=(int)e.UserState;
label1.Text=报告值;
//我们可以把这个.Refresh()放在这里强制重新绘制,这会给我们带来高的重新绘制,但我们会失败
//控制装置的所有其他响应性
}
void bw_DoWork(对象发送方,DoWorkEventArgs e)
{
对于(int x=0;x<100000;x++)
{     
//我们可以把Thread.Sleep放在这里,但我们不会得到最高性能的更新
bw.报告进度(0,x);
}
}
请参见代码中的注释。另外,请不要问我为什么要这个。问题很简单,我们如何在保持响应性的同时在更新表单时实现最高的保真度(大多数重绘)?强制重新绘制确实会为我们提供更新,但我们不会处理windows消息

我还尝试放置DoEvents,但这会产生堆栈溢出。我需要的是说“如果你最近没有处理任何windows消息”。我还可以看到,要实现这一点,可能需要一种稍微不同的模式

看来我们需要处理几个问题:

  • 通过非UI线程更新表单。对于这个问题有很多解决方案,例如调用、同步上下文、后台工作模式
  • 第二个问题是表单中充斥着太多的更新,这会阻碍消息处理,这是我的问题真正关心的问题。在大多数示例中,这是通过任意等待或仅每X%更新一次来降低请求速度的简单方法。这两种解决方案都不适用于实际应用程序,也不符合最大更新响应标准
  • 我对如何处理这个问题的一些初步想法:

  • 在后台工作程序中对项目进行排队,然后在UI线程中分派它们。这将确保每个项目都是油漆,但将导致滞后,我们不希望
  • 也许使用第三方物流
  • 可能在UI线程中使用计时器来指定刷新值。通过这种方式,我们可以以我们能够处理的最快速度获取数据。它需要跨线程访问/共享数据

  • 更新,我已经更新为使用计时器读取后台工作线程更新的共享变量。现在由于某种原因,这种方法产生了良好的表单响应,并且允许后台工作人员以同样快的速度更新大约1000倍。但有趣的是,它的准确度只有1毫秒

    因此,我们应该能够更改模式以读取当前时间并从bw线程调用更新,而不需要计时器

    以下是新模式:

    //Timer setup
    {
                RefreshTimer.SynchronizingObject = this;
                RefreshTimer.Elapsed += RefreshTimer_Elapsed;
                RefreshTimer.AutoReset = true;
                RefreshTimer.Start();
    }           
    
         void bw_DoWork(object sender, DoWorkEventArgs e)
                {
                        for (int x = 0; x < 1000000000; x++)
                        {                    
                           //bw.ReportProgress(0, x);                    
                           //mUiContext.Post(UpdateLabel, x);
                            SharedX = x;
                        }
                }
    
            void RefreshTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
            {
                label1.Text = SharedX.ToString();
            }
    
    //计时器设置
    {
    RefreshTimer.SynchronizingObject=此;
    RefreshTimer.appeased+=RefreshTimer\u appeased;
    RefreshTimer.AutoReset=true;
    RefreshTimer.Start();
    }           
    void bw_DoWork(对象发送方,DoWorkEventArgs e)
    {
    对于(int x=0;x<100000000;x++)
    {                    
    //bw.报告进度(0,x);
    //mUiContext.Post(UpdateLabel,x);
    SharedX=x;
    }
    }
    无效刷新计时器\u已过(对象发送器,System.Timers.ElapsedEventArgs e)
    {
    label1.Text=SharedX.ToString();
    }
    
    更新这里我们有了一个新的解决方案,它不需要计时器,也不阻塞线程!我们用这种模式实现了高性能的计算和更新的逼真度。不幸的是,ticks TickCount只精确到1毫秒,但是我们可以每毫秒运行一批X更新,以获得比1毫秒更快的计时

       void bw_DoWork(object sender, DoWorkEventArgs e)
        {
                long lastTickCount = Environment.TickCount;                
                for (int x = 0; x < 1000000000; x++)
                {
                    if (Environment.TickCount - lastTickCount > 1)
                    {
                        bw.ReportProgress(0, x);
                        lastTickCount = Environment.TickCount;
                    }                 
                }
        }
    
    void bw\u DoWork(对象发送方,DoWorkEventArgs e)
    {
    long lastTickCount=Environment.TickCount;
    对于(int x=0;x<100000000;x++)
    {
    如果(Environment.TickCount-lastTickCount>1)
    {
    bw.报告进度(0,x);
    lastTickCount=Environment.TickCount;
    }                 
    }
    }
    
    报告进度的速度要快于用户跟踪进度的速度,这没有什么意义

    如果您的后台线程发布消息的速度快于GUI处理消息的速度(并且您有所有的症状——GUI对用户输入的响应差,不存在失控的递归),那么您必须以某种方式限制更新的进度

    一种常见的方法是使用一个主线程形式的计时器以足够小的速率更新GUI,以便用户看到一个可接受的进度读数。您可能需要一个互斥或关键部分来保护共享数据,但如果要监视的进度值是int/uint,则不需要这样做


    另一种方法是强制线程阻塞事件或信号量,直到GUI空闲。

    尝试以用户无法跟踪的速度报告进度没有什么意义

    如果您的后台线程发布消息的速度快于GUI处理消息的速度(并且您有所有的症状——GUI对用户输入的响应差,不存在失控的递归),那么您必须以某种方式限制更新的进度

    一种常见的方法是使用一个主线程形式的计时器,以足够小的速率更新GUI,以便用户看到
    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Windows.Forms;
    
    namespace WinForms_21643584
    {
        public partial class MainForm : Form
        {
            EventHandler ContentChanged = delegate { };
    
            public MainForm()
            {
                InitializeComponent();
                this.Load += MainForm_Load;
            }
    
            // Update UI Task
            async Task DoUiWorkAsync(CancellationToken token)
            {
                try
                {
                    var startTick = Environment.TickCount;
                    var editorText = this.webBrowser.Document.Body.InnerText;
                    while (true)
                    {
                        // observe cancellation
                        token.ThrowIfCancellationRequested();
    
                        // throttle (optional)
                        await Task.Delay(50);
    
                        // yield to keep the UI responsive
                        await ApplicationExt.IdleYield();
    
                        // poll the content for changes
                        var newEditorText = this.webBrowser.Document.Body.InnerText;
                        if (newEditorText != editorText)
                        {
                            editorText = newEditorText;
                            this.status.Text = "Changed on " + (Environment.TickCount - startTick) + "ms";
                            this.ContentChanged(this, EventArgs.Empty);
                        }
                    }
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                }
            }
    
            async void MainForm_Load(object sender, EventArgs e)
            {
                // navigate the WebBrowser
                var documentTcs = new TaskCompletionSource<bool>();
                this.webBrowser.DocumentCompleted += (sIgnore, eIgnore) => documentTcs.TrySetResult(true);
                this.webBrowser.DocumentText = "<div style='width: 100%; height: 100%' contentEditable='true'></div>";
                await documentTcs.Task;
    
                // cancel updates in 10 s
                var cts = new CancellationTokenSource(20000);
    
                // start the UI update 
                var task = DoUiWorkAsync(cts.Token);
            }
        }
    
        // Yield via Application.Idle
        public static class ApplicationExt
        {
            public static Task<bool> IdleYield()
            {
                var idleTcs = new TaskCompletionSource<bool>();
                if (IsMessagePending())
                {
                    // register for Application.Idle
                    EventHandler handler = null;
                    handler = (s, e) =>
                    {
                        Application.Idle -= handler;
                        idleTcs.SetResult(true);
                    };
                    Application.Idle += handler;
                }
                else
                    idleTcs.SetResult(false);
                return idleTcs.Task;
            }
    
            public static bool IsMessagePending()
            {
                // The high-order word of the return value indicates the types of messages currently in the queue. 
                return 0 != (GetQueueStatus(QS_MASK) >> 16 & QS_MASK);
            }
    
            const uint QS_MASK = 0x1FF;
    
            [System.Runtime.InteropServices.DllImport("user32.dll")]
            static extern uint GetQueueStatus(uint flags);
        }
    }