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