如何正确地创建一个无限的工作线程?

10

我有一个对象需要进行大量初始化(在强力机器上需要1到2秒)。但是一旦初始化完成,它只需要约20毫秒来完成典型的“工作”。

为了防止每次应用程序想要使用它时都重新初始化该对象(在典型用法中可能是每秒50次或数分钟不使用),我决定给它一个工作队列,并在其自己的线程上运行,检查队列中是否有任何工作。 但是,我不完全确定如何创建一个无限运行的线程,无论是否有工作。

以下是我目前所拥有的,欢迎任何批评意见。

    private void DoWork()
    {
        while (true)
        {
            if (JobQue.Count > 0)
            {
                // do work on JobQue.Dequeue()
            }
            else
            {
                System.Threading.Thread.Sleep(50);
            }
        }
    }

事后想法:我在考虑是否需要优雅地终止此线程,而不是让它永远运行。因此,我认为我会添加一种作业类型,告诉线程结束。如何终止这样的线程的任何想法也受到欢迎。

6个回答

20

必须使用lock,这样你就可以使用WaitPulse

while(true) {
    SomeType item;
    lock(queue) {
        while(queue.Count == 0) {
            Monitor.Wait(queue); // releases lock, waits for a Pulse,
                                 // and re-acquires the lock
        }
        item = queue.Dequeue(); // we have the lock, and there's data
    }
    // process item **outside** of the lock
}

使用类似以下方式添加:

lock(queue) {
    queue.Enqueue(item);
    // if the queue was empty, the worker may be waiting - wake it up
    if(queue.Count == 1) { Monitor.PulseAll(queue); }
}

你可能还想看看这个问题,它可以限制队列的大小(如果队列已经满了,则进行阻塞)。


经典队列,锁和脉冲。 - scope_creep
1
@Will:据我所知,pfx 的唯一下载是 CTP - 即不完全支持。在 4.0 版本(目前为 beta 版)中,TPL 等将成为常态 - 但我们仍然会在这里收到很多2.0的问题... - Marc Gravell
马克,你能详细解释一下 Monitor.Wait 发生了什么吗?这看起来非常有趣。 - Neil N
@Neil - 我在另一个问题(链接)中也涵盖了更多关于退出条件的内容。 - Marc Gravell
1
好的回答,不过我知道Pulse和PulseAll被认为是一个错误。具体来说,如果你在内核调度程序线程切换期间执行其中一个操作,就可能会错过一个Pulse。 - Jan Bannister
显示剩余2条评论

3
您需要一种同步原语,例如WaitHandle(查看静态方法)。这样,您就可以“通知”工作线程有工作要做。它检查队列并继续工作,直到队列为空,此时它会等待互斥锁再次发出信号。
同时,将其中一个工作项设置为退出命令,以便在需要退出线程时向工作线程发出信号。

1

我已经实现了一个后台任务队列,而不使用任何类型的while循环、脉冲、等待或者触摸Thread对象。它似乎是有效的。(我的意思是,在过去的18个月中,它一直在生产环境中处理数千个任务,没有出现任何意外行为。)这是一个具有两个重要属性的类,即Queue<Task>BackgroundWorker。这里简略介绍了三个重要方法:

private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
   if (TaskQueue.Count > 0)
   {
      TaskQueue[0].Execute();
   }
}

private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    Task t = TaskQueue[0];

    lock (TaskQueue)
    {
        TaskQueue.Remove(t);
    }
    if (TaskQueue.Count > 0 && !BackgroundWorker.IsBusy)
    {
        BackgroundWorker.RunWorkerAsync();
    }
}

public void Enqueue(Task t)
{
   lock (TaskQueue)
   {
      TaskQueue.Add(t);
   }
   if (!BackgroundWorker.IsBusy)
   {
      BackgroundWorker.RunWorkerAsync();
   }
}

并不是没有等待和脉冲。但这一切都发生在BackgroundWorker内部。每当一个任务被放入队列中时,它就会被唤醒,运行直到队列为空,然后再次进入睡眠状态。

我远非线程方面的专家。如果使用BackgroundWorker可以解决问题,那么有必要去研究System.Threading吗?


总的来说,我同意BackgroundWorker类通常是管理后台任务的更简单选择。在这种情况下,我必须挑战你的一些说法。您正在使用线程对象:lock关键字是System.Threading.Monitor.Enter()System.Threading.Monitor.Exit()的语法糖。您的RunWorkerCompleted事件处理程序调用BackgroundWorker.RunWorkerAsync()而不是while循环。我认为,在这种情况下,等待/脉冲while循环的逻辑可能更容易理解。 - Don Kirkby
好的。可能只是因为我对等待/触发不熟悉,所以它看起来对我来说更简单。 - Robert Rossney

1
在大多数情况下,我所做的与您设置的方式非常相似,但语言不同。我有幸使用了一个数据结构(Python中的)来阻塞线程直到队列中放入一个项目,避免使用sleep调用。
如果.NET提供了这样的类,我会考虑使用它。 线程阻塞比线程在sleep调用上旋转要好得多。
您可以传递的作业可能只是一个“null”; 如果代码收到null,则知道该退出while并回家了。

1

获取并行框架。它有一个BlockingCollection<T>,您可以将其用作任务队列。使用方法如下:

  1. 创建一个BlockingCollection<T>,用于保存您的任务。
  2. 创建一些线程,这些线程具有永不停止的循环(while(true){ // get job off the queue)
  3. 启动线程
  4. 当任务/工作可用时,将其添加到集合中

线程将被阻塞,直到集合中出现项目。 轮到谁获取它(取决于CPU)。 我现在正在使用它,效果很好。

它还具有依赖于MS编写的特别恶心的代码的优点,其中多个线程访问同一资源。 每当你能让别人为你编写它时,你应该去做。 当然,假设他们比您拥有更多的技术/测试资源和经验。


你是指标有"This CTP is for testing purposes only."的CTP吗?不确定这是一个好建议...在4.0中,可以 - 但那是测试版! - Marc Gravell

1

如果你不是真的需要使线程退出(而只是希望它不会让应用程序一直运行),你可以将Thread.IsBackground设置为true,当所有非后台线程结束时,它就会结束。Will和Marc都有处理队列的好方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接