2010-07-13 33 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