1

让我设置这个问题与一些背景信息,我们有一个长期运行的过程,将在Windows窗体中生成数据。所以,显然需要某种形式的多线程来保持表单的响应。但是,我们也有要求,表格每秒更新多次,同时仍然保持响应。Winforms更新与高性能

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

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

请参阅代码中的注释。另外,请不要质疑我为什么要这样做。这个问题很简单,我们如何在保持响应性的同时更新表单来实现最高保真度(大多数重绘)?强制重新绘制确实给了我们更新,但我们不处理Windows消息。

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

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

  1. 通过非UI线程更新表格。这个问题有很多解决方案,例如invoke,同步上下文,后台工作模式。
  2. 第二个问题是充斥着太多阻止消息处理的更新窗体,这是我的问题真正关注的问题。在大多数示例中,通过使用任意等待减慢请求或仅更新每个X%来简化处理。这些解决方案都不适用于真实世界的应用程序,也不符合响应标准的最大更新。

我的一些关于如何处理这个初始的想法:

  1. 队列中后台工作项目,然后派遣他们在UI线程。这将确保每个项目都被绘制,但会导致我们不想要的滞后。
  2. 也许使用TPL
  3. 也许在UI线程中使用一个定时器来指定刷新值。通过这种方式,我们可以以我们可以处理的最快速度获取数据。它将要求跨线程访问/共享数据。

更新,我已经更新了使用计时器来读取一个共享变量与背景工作者线程更新。现在由于某种原因,这种方法产生了良好的表单响应,并且允许后台工作人员以最快的速度更新大约1,000倍。但是,有趣的是,它只有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(); 
     } 

更新在这里,我们有不需要计时器,并不会阻止该线程的新的解决方案!我们在计算和高保真度模式下实现更新。不幸的是,tickCount只有1 MS精度,但是我们可以对每个MS运行一批X更新,以便获得更快的1 MS定时。

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

你试图做的是非常毫无意义和自我挫败。你只是这样做才能保持人性化的乐趣。除了超出每秒25次更新的模糊之外,它无法感知任何东西。做得更快只是浪费CPU周期。而自我挫败,因为如果你做得太快(每秒约1000次),那么UI线程会变得紧张,试图跟上并且永远无法赶上。并停止绘画并响应输入。 –

回答

4

试图以比用户可以跟踪的速度更快的速度报告进度是毫无意义的。

如果您的后台线程发布消息的速度超过了GUI可以处理它们的速度(并且您拥有所有这些 - 可怜的GUI响应用户输入,DoEvents失控递归),您必须以某种方式限制进度更新。

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

另一种方法是通过强制它阻塞事件或信号量直到GUI空闲来扼杀线程。

2

UI线程不应该由CPU绑定操作持续超过50ms("The 50ms Rule")。通常,UI工作项在事件时执行,由用户输入触发,完成IO绑定操作或卸载到后台线程的CPU绑定操作。

但是,有一些罕见的情况,当工作需要在UI线程上完成。例如,您可能需要轮询UI控件以进行更改,因为该控件不会公开适当的onchange风格的事件。特别是,这适用于WebBrowser控制(DOM Mutation Observers只是被引入,并且IHTMLChangeSink并不总是可靠工作,根据我的经验)。

下面是如何有效地完成它,而不会阻塞UI线程消息队列。一些关键的东西用在这里做到这一点:

  • 的UI工作任务收益率(通过Application.Idle)来处理任何未决的消息
  • GetQueueStatus用于对是否产生决定或不
  • Task.Delay用于节制循环,类似于计时器事件。如果轮询需要尽可能精确,则此步骤是可选的。
  • async/await提供伪同步线性码流。

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

该代码是特定于的WinForms。这是一个similar approach for WPF