2010-07-13 65 views
1

我在使用生產者/消費者隊列結構的多線程下載器上修修補補;下載部分工作正常,但我遇到了一個問題,保持GUI更新。如何強制主GUI線程在更新後從其他線程更新列表框?

現在我使用窗體上的列表框控件來顯示狀態消息和下載進度的更新,最終我希望用progressbar來替換它。

首先將Form1代碼放在後面;該表格包含只是一個按鈕,列表框:

public partial class Form1 : Form 
{ 
    public Form1() 
    { 
     InitializeComponent(); 
    } 

    public void SetProgressMessage(string message) 
    { 
     if (listboxProgressMessages.InvokeRequired) 
     { 
      listboxProgressMessages.Invoke(new MethodInvoker(delegate() 
      { SetProgressMessage(message); })); 
     } 
     else 
     { 
      listboxProgressMessages.Items.Add(message); 
      listboxProgressMessages.Update(); 
     } 
    } 

    private void buttonDownload_Click(object sender, EventArgs e) 
    { 
     SetProgressMessage("Enqueueing tasks"); 
     using (TaskQueue q = new TaskQueue(4)) 
     { 
      q.EnqueueTask("url"); 
      q.EnqueueTask("url"); 
      q.EnqueueTask("url"); 
      q.EnqueueTask("url"); 
      q.EnqueueTask("url"); 
      q.EnqueueTask("url"); 
      q.EnqueueTask("url"); 
      q.EnqueueTask("url"); 
      q.EnqueueTask("url"); 
      q.EnqueueTask("url"); 
     } 
     SetProgressMessage("All done!"); 
    } 
} 

現在生產者/消費者邏輯。消費者一點一點地下載文件,並且應該告訴居住在GUI線程上的列表框如何進展;這是有效的,但是在完成所有操作之前,列表框並沒有實際更新,還有消息出現在「全部完成」之後。消息,這是不可取的。

TaskQueue.cs:

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Threading; 
using System.IO; 
using System.Net; 
using System.Windows.Forms; 
using System.Runtime.Remoting.Messaging; 

namespace WinformProducerConsumer 
{ 
    public class TaskQueue : IDisposable 
    { 
     object queuelocker = new object(); 
     Thread[] workers; 
     Queue<string> taskQ = new Queue<string>(); 

    public TaskQueue(int workerCount) 
    { 
     workers = new Thread[workerCount]; 
     for (int i = 0; i < workerCount; i++) 
      (workers[i] = new Thread(Consume)).Start(); 
    } 

    public void Dispose() 
    { 
     foreach (Thread worker in workers) EnqueueTask(null); 
     foreach (Thread worker in workers) worker.Join(); 
    } 

    public void EnqueueTask(string task) 
    { 
     lock (queuelocker) 
     { 
      taskQ.Enqueue(task); 
      Monitor.PulseAll(queuelocker); 
     } 
    } 

    void Consume() 
    { 
     while (true) 
     { 
      string task; 
      Random random = new Random(1); 
      lock (queuelocker) 
      { 
       while (taskQ.Count == 0) Monitor.Wait(queuelocker); 
       task = taskQ.Dequeue(); 
      } 
      if (task == null) return; 

      try 
      { 
       Uri url = new Uri(task); 
       HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); 
       HttpWebResponse response = (HttpWebResponse)request.GetResponse(); 
       response.Close(); 
       Int64 iSize = response.ContentLength; 
       Int64 iRunningByteTotal = 0; 

       using (WebClient client = new System.Net.WebClient()) 
       { 
        using (Stream streamRemote = client.OpenRead(new Uri(task))) 
        { 
         using (Stream streamLocal = new FileStream(@"images\" + Path.GetFileName(task), FileMode.Create, FileAccess.Write, FileShare.None)) 
         { 
          int iByteSize = 0; 
          byte[] byteBuffer = new byte[iSize]; 
          while ((iByteSize = streamRemote.Read(byteBuffer, 0, byteBuffer.Length)) > 0) 
          { 
           streamLocal.Write(byteBuffer, 0, iByteSize); 
           iRunningByteTotal += iByteSize; 

           double dIndex = (double)iRunningByteTotal; 
           double dTotal = (double)byteBuffer.Length; 
           double dProgressPercentage = (dIndex/dTotal); 
           int iProgressPercentage = (int)(dProgressPercentage * 100); 

           string message = String.Format("Thread: {0} Done: {1}% File: {2}", 
            Thread.CurrentThread.ManagedThreadId, 
            iProgressPercentage, 
            task); 

           Form1 frm1 = (Form1)FindOpenForm(typeof(Form1)); 
           frm1.BeginInvoke(new MethodInvoker(delegate() 
           { 
            frm1.SetProgressMessage(message); 
           })); 
          } 
          streamLocal.Close(); 
         } 
         streamRemote.Close(); 
        } 
       } 

      } 
      catch (Exception ex) 
      { 
       // Generate message for user 
      } 
     } 
    } 

    private static Form FindOpenForm(Type typ) 
    { 
     for (int i1 = 0; i1 < Application.OpenForms.Count; i1++) 
     { 
      if (!Application.OpenForms[i1].IsDisposed && (Application.OpenForms[i1].GetType() == typ)) 
      { 
       return Application.OpenForms[i1]; 
      } 
     } 
     return null; 
    } 
} 

}

任何建議,例子嗎?我四處尋找解決方案,但找不到任何我可以遵循或工作的東西。 。

更換frm1.BeginInvoke(新MethodInvoker(委託()與frm1.Invoke(新MethodInvoker(委託()死鎖結果,我願意在這裏難倒

來源: 生產者/消費者例如:http://www.albahari.com/threading/part4.aspx

更新:我以錯誤的方式解決問題;而不是從工作線程調回GUI,我將使用GUI線程必須留意的事件。經驗教訓:)

回答

2

你應該消除FindOpenFormProgressChanged事件添加到TaskQueue。它是絕對不是 TaskQueue責任直接調用表示層(窗體或控件)。這是形式的責任,聽取任務產生的「進度變更」事件,然後更新自己的財產。

這將很容易解決您的問題,它會保持簡單,遵循最佳實踐並消除時間問題,線程問題等。

+0

好吧,我會在這個方向上尋找解決方案。 :) – MartijnK 2010-07-13 12:44:34

+0

解決方案非常簡單。僞代碼。公共事件progresschange作爲事件處理程序。刪除findopenform,替換爲raise progresschange。在表單中,addhandler q.progresschange,address_reportprogresschange的地址。在form_reportprogresschange中調用SetProgressMessage。 – AMissico 2010-07-13 12:48:00

+0

我很晚了,但通過這個工作。這是一個很好的編程練習,也是一個有趣的.NET技巧。 – AMissico 2010-07-13 12:51:54

1

我的另一個更適合。一旦您更改TaskQueue以提升ProgressChanged事件,此答案更合適。


嘗試撥打listboxProgressMessages.Refresh()。這迫使油漆。檢查Control.Refresh文檔。有時候,你必須調用表單的刷新方法。

Control.Refresh方法

強制控制無效的 客戶區域並立即重繪 自己和任何子控件。

http://msdn.microsoft.com/en-us/library/system.windows.forms.control.refresh.aspx

+0

我試過.Refresh()和.Update();所有事情完成後他們都會離開。換句話說,直到「全部完成!」纔會發生。出現,然後添加進度消息並在添加每行後刷新列表框多次。 我認爲這是一個計時的事情;看起來,在生產者/消費者隊列的使用中,消費者不能回傳給gui線程。 – MartijnK 2010-07-13 12:22:11

+0

線程可能不啓動。 (它們在系統分配時間時開始,並不立即開始。)保持刷新。在刷新之後放置一個睡眠(250)用於調試目的。在每個EnqueueTask後放置一個Sleep(110)。確保每個任務需要5000 + ms。任何時機問題都會「跳出」你。 (睡眠時間必須超過100ms。)如果這不起作用,則上一個故障排除步驟是用DoEvents替換睡眠。如果它仍然不能與DoEvents一起使用,那麼你還有其他事情與表單無關。 – AMissico 2010-07-13 12:33:56

+0

線程開始;我發現文件應該出現在/ images中,所以不用擔心。 如果我使用Invoke(死鎖?),它會停下來。如果我使用BeginInvoke,則顯示會延遲到所有下載完成後。 – MartijnK 2010-07-13 12:34:08

0
在你的代碼

此外,而不是調用BeginInvoke,叫Invoke - 後者是從調用者同步。這意味着你需要通過GUI調用並等待GUI返回之前完成其操作。

此外,GUI線程可能會餓死,我認爲調用到GUI線程的控件實際上是編組到消息泵 - 如果泵不泵,那麼調用永遠不會通過。可以使用BackgroundWorker表單組件。這已經被構建爲在WinForms中工作,從而與GUI一起工作。它公開了一個ProgressChanged事件,您可以使用該事件將任意進度報告傳遞迴UI。事件本身被編組,以便自動調用GUI線程。這消除了您手動執行此操作的需要。

BackgroundWorker線程也使用線程池。

+0

我考慮過並嘗試過它,但它會讓應用停下來;可能是一個僵局。兩個文件出現在下載目錄中(即使有兩個以上的工作線程分配給使用隊列),但它們仍保持在0字節的大小。如果沒有Invoke/BeginInvoke /方法調用,線程會按照它們應該做的那樣去做。 任何建議或從工作線程獲取信息回到GUI線程的替代方法? – MartijnK 2010-07-13 12:42:43

+0

是的,你可以使用BackgroundWorker。我會修改我的答案。 – 2010-07-13 13:00:34

+0

謝謝你的建議。 :) – MartijnK 2010-07-13 13:05:50