Winforms updates with high performance

2019-01-15 22:49发布

问题:

Let me setup this question with some background information, we have a long running process which will be generating data in a Windows Form. So, obviously some form of multi-threading is going to be needed to keep the form responsive. But, we also have the requirement that the form updates as many times per second while still remaining responsive.

Here is a simple test example using background worker thread:

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);                    
            }
    }

Please see the comments in the code. Also, please don't question why I want this. The question is simple, how do we achieve the highest fidelity (most repaints) in updating the form while maintaining responsiveness? Forcing the repaint does give us updates but we don't process windows messages.

I have also try placing DoEvents but that produces stack overflow. What I need is some way to say, "process any windows messages if you haven't lately". I can see also that maybe a slightly different pattern is needed to achieve this.

It seems we need to handle a few issues:

  1. Updating the Form through the non UI thread. There are quite a few solution to this problem such as invoke, synchronization context, background worker pattern.
  2. The second problem is flooding the Form with too many updates which blocks the message processing and this is the issue around which my question really concerns. In most examples, this is handles trivially by slowing down the requests with an arbitrary wait or only updating every X%. Neither of these solutions are approriate for real-world applications nor do they meet the maximum update while responsive criteria.

Some of my initial ideas on how to handle this:

  1. Queue the items in the background worker and then dispatch them in a UI thread. This will ensure every item is painted but will result in lag which we don't want.
  2. Perhaps use TPL
  3. Perhaps use a timer in the UI thread to specify a refresh value. In this way, we can grab the data at the fastest rate that we can process. It will require accessing/sharing data across threads.

Update, I've updated to use a Timer to read a shared variable with the Background worker thread updates. Now for some reason, this method produces a good form response and also allows the background worker to update about 1,000x as fast. But, interestingly it only 1 millisecond accurate.

So we should be able to change the pattern to read the current time and call the updates from the bw thread without the need for the timer.

Here is the new pattern:

//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();
        }

Update And here we have the new solution that doesn't require the timer and doesn't block the thread! We achieve a high performance in calculations and fidelity on the updates with this pattern. Unfortunately, ticks TickCount is only 1 MS accurate, however we can run a batch of X updates per MS to get faster then 1 MS timing.

   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;
                }                 
            }
    }

回答1:

There is little point in trying to report progress any faster than the user can keep track of it.

If your background thread is posting messages faster than the GUI can process them, (and you have all the symtoms of this - poor GUI resonse to user input, DoEvents runaway recursion), you have to throttle the progress updates somehow.

A common approach is to update the GUI using a main-thread form timer at a rate sufficiently small that the user sees an acceptable progress readout. You may need a mutex or critical section to protect shared data, though that amy not be necessary if the progress value to be monitored is an int/uint.

An alternative is to strangle the thread by forcing it to block on an event or semaphore until the GUI is idle.



回答2:

The UI thread should not be held for more than 50ms by a CPU-bound operation taking place on it ("The 50ms Rule"). Usually, the UI work items are executed upon events, triggered by user input, completion of an IO-bound operation or a CPU-bound operation offloaded to a background thread.

However, there are some rare cases when the work needs to be done on the UI thread. For example, you may need to poll a UI control for changes, because the control doesn't expose proper onchange-style event. Particularly, this applies to WebBrowser control (DOM Mutation Observers are only being introduced, and IHTMLChangeSink doesn't always work reliably, in my experience).

Here is how it can be done efficiently, without blocking the UI thread message queue. A few key things was used here to make this happen:

  • The UI work tasks yields (via Application.Idle) to process any pending messages
  • GetQueueStatus is used to decide on whether to yield or not
  • Task.Delay is used to throttle the loop, similar to a timer event. This step is optional, if the polling needs to be as precise as possible.
  • async/await provide pseudo-synchronous linear code flow.

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);
    }
}

This code is specific to WinForms. Here is a similar approach for WPF.