Как заставить основной поток GUI обновить список после его обновления из другого потока? - PullRequest
1 голос
/ 13 июля 2010

Я работаю над многопоточным загрузчиком, используя конструкцию очереди производителя / потребителя; части загрузки работают нормально, но у меня проблема с обновлением графического интерфейса.

Пока что я использую элемент управления списком в форме для отображения сообщений о состоянии и обновлений о ходе загрузки, в конце концов я надеюсь заменить его на индикаторы выполнения.

Сначала код 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. Урок усвоен. :)

Ответы [ 3 ]

2 голосов
/ 13 июля 2010

Вы должны исключить FindOpenForm и добавить событие ProgressChanged к TaskQueue. абсолютно не Задача TaskQueue отвечает за прямые вызовы на уровне представления (формы или элементы управления).В этом случае ответственность формы заключается в том, чтобы прослушивать события «изменения прогресса», сгенерированные задачами, а затем корректно обновлять себя.

Это легко решит вашу проблему, сохранит простоту, будет следовать рекомендациям и устранитпроблемы синхронизации, потоков и т. д.

1 голос
/ 13 июля 2010

Мой другой ответ более уместен. Этот ответ более уместен, если вы измените TaskQueue для вызова события ProgressChanged.


Попробуйте позвонить listboxProgressMessages.Refresh(). Это заставляет краску. Проверьте Control.Refresh документацию. Иногда вам нужно вызвать метод обновления формы.

Control.Refresh Метод

Заставляет элемент управления аннулировать его область клиента и сразу перерисовать сам и любой дочерний элемент управления.

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

0 голосов
/ 13 июля 2010

Также в вашем коде вместо вызова BeginInvoke вызовите Invoke - последний синхронен с вызывающим абонентом.Это будет означать, что вы обращаетесь к GUI и ждете, пока GUI не завершит свою работу, прежде чем вернуться.

Кроме того, поток GUI может быть истощен, я думаю, что вызовы в поток GUI для элементов управления фактически маршализованына насос сообщений - если насос не перекачивает, вызов так и не поступит.

Обновление: в качестве альтернативы, вы можете использовать компонент формы BackgroundWorker.Это было построено для работы в WinForms, таким образом, работая с GUI.Он предоставляет событие ProgressChanged, которое можно использовать для передачи произвольных отчетов о ходе выполнения обратно в пользовательский интерфейс.Само событие распределяется так, что оно вызывает поток GUI автоматически.Это избавит вас от необходимости делать это вручную. Потоки потоков

BackgroundWorker также используют пул потоков.

...