基于程序描述的多线程推荐

时间:2010-03-10 15:41:05

标签: .net multithreading

我想描述一下我的程序的一些细节,并获得最适合使用的最佳多线程模型的反馈。我花了很多时间阅读ThreadPool,Threads,Producer / Consumer等等,并且尚未得出可靠的结论。

我有一个文件列表(所有格式相同)但内容不同。我必须对每个文件执行工作。这项工作包括读取文件,一些处理需要大约1-2分钟的直接数字处理,然后在最后编写大量输出文件。

我希望在我启动指定文件的工作后,UI界面仍然具有响应性。

有些问题:

  1. 我应该使用哪种型号/机制?制片人/消费者,工作游戏等
  2. 我是否应该在UI中使用BackgroundWorker以获得响应,或者我可以从表单中启动线程,只要我单独保留UI线程以继续响应用户输入?
  3. 我如何在每个文件上获取每个单独工作的结果或状态,并以线程安全的方式将其报告给UI,以便在工作进展时提供用户反馈(可以处理近1000个文件)
  4. 更新

    到目前为止反馈很好,非常有帮助。我正在添加下面提到的更多细节:

    • 输出到多个独立文件。每个“工作项”的一组输出文件,然后在“工作项”完成之前由另一个进程读取和处理

    • 工作项/主题不共享任何资源。

    • 使用非托管静态库部分处理工作项,该库使用boost库。

5 个答案:

答案 0 :(得分:1)

通常,您应该使用BackgroundWorker进行UI的后台处理,因为这是该类专门设计的内容。通常,线程池用于服务器应用程序。

您可以尝试使用多个BackgroundWorkers来完成您需要做的事情。只需将所有文件添加到队列中,然后生成BackgroundWorker以从队列中读取并处理下一个文件。你最多可能会产生n个工人来处理多个文件;您只需要一些方法来跟踪哪个工作人员正在处理每个文件,以便您向UI报告有意义的进度。

要确定每个工作人员正在做什么工作,您可以将参数传递给标识该线程的RunWorkerAsync。然后可以通过DoWork属性在DoWorkEventArgs.Argument中访问该参数。要知道哪个工作人员报告了进度,您可以单独为每个工作者添加一个事件处理程序和/或将一个对象传递给标识该工作者的ReportProgress

这有帮助吗?

答案 1 :(得分:1)

根据评论进行更新
我不同意ThreadPool无法处理您遇到的工作负载的说法......让我们看一下您的问题并获得更具体的信息:
你有近1000个文件 2.每个文件最多可能需要2分钟的CPU密集型工作才能处理 3.您希望进行并行处理以提高吞吐量 4.您希望在每个文件完成时发出信号并更新UI。

实际上你不想运行1000个线程,因为你受到核心数量的限制......而且由于它是CPU密集型工作,你很可能用很少的线程来最大化CPU负载(在我的程序通常最好每个核心有2-4个线程。

因此,您不应在ThreadPool中加载1000个工作项,并且期望看到吞吐量增加。您必须创建一个环境,在该环境中,您始终使用最佳线程数运行,这需要一些工程设计。

我将不得不与我原来的陈述相矛盾,并且实际上推荐了制作人/消费者设计。有关模式的更多详细信息,请查看此question

以下是制作人的样子:

class Producer
{
    private final CountDownLatch _latch;
    private final BlockingQueue _workQueue;
    Producer( CountDownLatch latch, BlockingQueue workQueue)
    {
        _latch = latch;
        _workQueue = workQueue;
    }

    public void Run()
    {
        while(hasMoreFiles)
        {
            // load the file and enqueue it
            _workQueue.Enqueue(nextFileJob);
        }

        _latch.Signal();
    }
}

这是您的消费者:

class Consumer
{
    private final CountDownLatch _latch;
    private final BlockingQueue _workQueue;

    Consumer(CountDownLatch latch, BlockingQueue workQueue, ReportStatusToUI reportDelegate)
    {
        _latch = latch;
        _workQueue = workQueue;
    }

    public void Run()
    {
        while(!terminationCondition)
        {
            // blocks until there is something in the queue
            WorkItem workItem = _workQueue.Dequeue();

            // Work that takes 1-2 minutes
            DoWork(workItem);

            // a delegate that is executed on the UI (use BeginInvoke on the UI)
            reportDelegate(someStatusIndicator);
        }

        _latch.Signal();
    }
}

A CountDownLatch

public class CountDownLatch
{
    private int m_remain;
    private EventWaitHandle m_event;

    public CountDownLatch(int count)
    {
        Reset(count);
    }

    public void Reset(int count)
    {
        if (count < 0)
            throw new ArgumentOutOfRangeException();
        m_remain = count;
        m_event = new ManualResetEvent(false);
        if (m_remain == 0)
        {
            m_event.Set();
        }
    }

    public void Signal()
    {
        // The last thread to signal also sets the event.
        if (Interlocked.Decrement(ref m_remain) == 0)
            m_event.Set();
    }

    public void Wait()
    {
        m_event.WaitOne();
    }
}

Jicksa's BlockingQueue

class BlockingQueue<T> {
    private Queue<T> q = new Queue<T>();

    public void Enqueue(T element) {
        q.Enqueue(element);
        lock (q) {
            Monitor.Pulse(q);
        }
    }

    public T Dequeue() {
        lock(q) {
            while (q.Count == 0) {
                Monitor.Wait(q);
            }
            return q.Dequeue();
        }
    }
}

那又是什么呢?那么现在你所要做的就是开始你所有的线程... 你可以用ThreadPool开始,BackgroundWorker开始,或者每个开始new Thread 它没有任何区别

根据您拥有的核心数量(每个核心大约2-4个消费者),您只需要创建一个ProducerConsumers的最佳数量。

父线程( NOT 您的UI线程)应该阻塞,直到完成所有使用者线程:

void StartThreads()
{
    CountDownLatch latch = new CountDownLatch(numConsumer+numProducer);
    BlockingQueue<T> workQueue = new BlockingQueue<T>();

    Producer producer = new Producer(latch, workQueue);
    if(youLikeThreads)
    {
        Thread p = new Thread(producer.Run);
        p.IsBackground = true;
        p.Start();
    }
    else if(youLikeThreadPools)
    {
        ThreadPool.QueueUserWorkItem(producer.Run);
    }

    for (int i; i < numConsumers; ++i)
    {
        Consumer consumer = new Consumer(latch, workQueue, theDelegate);

        if(youLikeThreads)
        {
            Thread c = new Thread(consumer.Run);

            c.IsBackground = true;

            c.Start();
        }
        else if(youLikeThreadPools)
        {
            ThreadPool.QueueUserWorkItem(consumer.Run);
        }
    }

    // wait for all the threads to signal
    latch.Wait();

    SayHelloToTheUI();
}

请注意,上述代码仅供参考。您仍然需要向ConsumerProducer发送终止信号,并且您需要以线程安全的方式执行此操作。

答案 2 :(得分:1)

我不会使用后台工作程序 - 它将您的处理与Winform UI层联系起来。如果要创建一个处理线程和处理的非可视类,最好使用Threadpool。

我会使用Threadpool与“直接”线程,因为.Net将对池进行一些负载平衡,并且它会回收线程,这样您就不必承担创建线程的成本。

如果您使用的是.Net 4,您可能会看一下新的并行线程库,我认为它包装了许多生产者/消费者的东西。

您可能希望使用某种“限制”来控制处理文件的速度(您可能不希望所有1000个文件一次性加载到内存中等)。您可以考虑一个生产者/消费者模式,您可以在其中控制一次处理的线程数。

对于返回UI的线程安全更新,请使用Winforms控件上的InvokeRequired和Invoke / BeginInvoke成员。

编辑 - 代码示例 我的例子比Lirik的简单,但也没有那么多。如果你需要一个完整的生产者/消费者,那就去Lirik所写的。从您的问题来看,您似乎想要构建一个文件列表,并将它们转移到其他组件,并让这些文件在后台处理。如果这就是你想做的全部,你可能不需要一个完整的生产者/消费者。

我假设这是某种批处理操作,一旦用户启动它,它们就不会在批处理完成之前添加更多文件。如果不是这样,那么生产者/消费者可能会更好。

此示例可以与Winform一起使用,但您不必使用。您可以在服务,控制台应用程序等中使用此组件:


    public class FileProcessor
    {
        private int MaxThreads = System.Environment.ProcessorCount;
        private volatile int ActiveWorkers;

        // you could define your own handler here to pass completion stats
        public event System.EventHandler FileProcessed;

        public event System.EventHandler Finished;

        private readonly object LockObj = new object();
        private System.Collections.Generic.Queue Files;

        public void ProcessFiles(System.Collections.Generic.Queue files)
        {
            this.Files = files;
            for (int i = 0; i < this.MaxThreads; i++)
                System.Threading.ThreadPool.QueueUserWorkItem(this.ProcessFile);
        }

        private void ProcessFile(object state)
        {
            this.IncrementActiveWorkers();
            string file = this.DequeueNextFile();
            while (file != null)
            {
                this.DoYourWork(file);
                this.OnFileProcessed(file);
                file = this.DequeueNextFile();
            } 
            // no more files left in the queue
            int workers = this.DecrementActiveWorkers();
            if (workers == 0)
                this.OnFinished();
        }

        // please give me a name!
        private void DoYourWork(string fileName) { }

        private void IncrementActiveWorkers()
        {
            lock (this.LockObj)
            {
                this.ActiveWorkers++;
            }
        }

        private int DecrementActiveWorkers()
        {
            lock (this.LockObj)
            {
                this.ActiveWorkers--;
                return this.ActiveWorkers;
            }
        }

        private string DequeueNextFile()
        {
            lock (this.LockObj)
            {
                // check for items available in queue
                if (this.Files.Count > 0)
                    return this.Files.Dequeue();
                else
                    return null;
            }

        }

        private void OnFileProcessed(string fileName)
        {
            System.EventHandler fileProcessed = this.FileProcessed;
            if (fileProcessed != null)
                fileProcessed(this, System.EventArgs.Empty);
        }

        private void OnFinished()
        {
            System.EventHandler finished = this.Finished;
            if (finished != null)
                finished(this, System.EventArgs.Empty);
        }
    }

由于您说“指定文件”,我假设您的Winform应用程序具有某种网格或列表框或用户与之交互的其他控件以选择要处理的文件。

以下是如何使用它的示例:


public class MyForm...
{
  public void Go()
  {
     Queue files = new Queue();
     // enqueue the name/path of all selected files into the queue...
     // now process them
     FileProcessor fp = new FileProcessor();

     // example of using an event
     fp.Finished += this.FileProcessor_Finished;

     fp.ProcessFiles(files);
  }

  private void FileProcessor_Finished(object sender, System.EventArgs e)
  {
     // this event will have been called by a non-ui thread.  Marshal it back to the UI
     if(this.InvokeRequired)
       this.Invoke(FileProcessor_Finished, new object[] {sender, e});
     else
     {
        // handle the event -- this will be run on the UI thread.
     }
  }
}

答案 3 :(得分:0)

我同意Justin Ethier的看法。 BackgroundWorker是一款易于使用的线程工具。

我知道您正面临着一种情况,您不知道要使用哪种线程模型。因此,它取决于您正在使用的对象。让我解释一下。

即使你想使用一个粗心的线程模型,开发人员不需要担心线程安全,如果你的对象或库不是线程安全的,你需要在这些对象之前使用lock()它们可以用于以下线程。例如,.NET 3.5集合不是线程安全的。

这里有related question应该有所帮助,此外还有Eric Lippert自己的解释!我还建议您看到他的blog on MSDN

希望这有帮助!

答案 4 :(得分:0)

BackhgroundWorker听起来很合理 主要问题是有多少应并行运行,因为您的任务似乎更多IO,然后CPU容易出现,而且您可能通过读取和写入不同的IO设备获得。