阻塞队列和多线程使用者,如何知道何时停止


69

我有一个单一的线程生产者,它创建一些任务对象,然后将它们添加到一个ArrayBlockingQueue(具有固定大小)中。

我还启动了一个多线程使用者。这是作为固定线程池(Executors.newFixedThreadPool(threadCount);)构建的。然后,我向该threadPool提交一些ConsumerWorker实例,每个ConsumerWorker都引用了上述ArrayBlockingQueue实例。

每个这样的Worker将take()在队列中执行并处理任务。

我的问题是,什么时候不再需要其他工作,让工人知道的最佳方法是什么。换句话说,我如何告诉工人生产者已完成添加到队列,从这一点开始,每个工人在看到队列为空时都应停止。

我现在得到的是一个设置,其中,生产者使用一个回调初始化,当他完成工作(向队列中添加内容)时触发该回调。我还保留了我创建并提交给ThreadPool的所有ConsumerWorkers的列表。当生产者回叫告诉我生产者完成时,我可以将此告知每个工人。在这一点上,他们应该继续检查队列是否不为空,当队列为空时,它们应该停止,从而使我能够正常关闭ExecutorService线程池。是这样的

public class ConsumerWorker implements Runnable{

private BlockingQueue<Produced> inputQueue;
private volatile boolean isRunning = true;

public ConsumerWorker(BlockingQueue<Produced> inputQueue) {
    this.inputQueue = inputQueue;
}

@Override
public void run() {
    //worker loop keeps taking en element from the queue as long as the producer is still running or as 
    //long as the queue is not empty:
    while(isRunning || !inputQueue.isEmpty()) {
        System.out.println("Consumer "+Thread.currentThread().getName()+" START");
        try {
            Object queueElement = inputQueue.take();
            //process queueElement
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

//this is used to signal from the main thread that he producer has finished adding stuff to the queue
public void setRunning(boolean isRunning) {
    this.isRunning = isRunning;
}

}

这里的问题是我有一个明显的竞争条件,有时候生产者会完成,发信号,而ConsumerWorkers会在消耗队列中的所有内容之前停止。

我的问题是什么是最好的同步方式,以便一切正常?我是否应该同步检查生产者是否正在运行的整个部分,以及队列是否为空,并在一个块中(在队列对象上)从队列中取出内容?我是否应该仅isRunning在ConsumerWorker实例上同步布尔值的更新?还有其他建议吗?

更新,这里是我最终使用的工作实现:

public class ConsumerWorker implements Runnable{

private BlockingQueue<Produced> inputQueue;

private final static Produced POISON = new Produced(-1); 

public ConsumerWorker(BlockingQueue<Produced> inputQueue) {
    this.inputQueue = inputQueue;
}

@Override
public void run() {
    //worker loop keeps taking en element from the queue as long as the producer is still running or as 
    //long as the queue is not empty:
    while(true) {
        System.out.println("Consumer "+Thread.currentThread().getName()+" START");
        try {
            Produced queueElement = inputQueue.take();
            Thread.sleep(new Random().nextInt(100));
            if(queueElement==POISON) {
                break;
            }
            //process queueElement
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Consumer "+Thread.currentThread().getName()+" END");
    }
}

//this is used to signal from the main thread that he producer has finished adding stuff to the queue
public void stopRunning() {
    try {
        inputQueue.put(POISON);
    } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

}

这主要是受到JohnVint的以下回答的启发,仅作了一些小改动。

===由于@vendhan的评论而进行了更新。

谢谢您的观察。没错,这个问题的第一个代码片段(以及其他问题)while(isRunning || !inputQueue.isEmpty())确实没有什么意义。

在此的最终实际实现中,我所做的事情与您建议替换“ ||”更接近 (或)与“ &&”(和),从某种意义上说,每个工人(消费者)现在仅检查从列表中得到的元素是否为毒丸,如果停止,则从理论上讲(所以理论上我们可以说该工人具有且队列不能为空)。


1
executorService已经有一个队列,因此您不需要另一个。您可以使用shutdown()启动整个执行程序服务。
彼得·劳瑞

@PeterLawrey对不起,但我不明白您的评论...
Shivan Dragon 2012年

8
由于ExecutorService已经有一个队列,因此您只需向其中添加任务即可,并且不需要其他队列,也不必弄清楚如何停止它们,因为已经实现了该队列。
彼得·劳瑞

没错,但是我想避免必须处理所有那些Callable和Runnable对象,我只想要一个包含实际业务数据的队列。但是,我将检查这样做是否不会导致更快的实现,然后再检查是否将阻塞队列的事情带给他们。
Shivan Dragon

1
@ShivanDragon:我提出了一个关联的问题,人们暗示您的OP应该具有&&而不是||的条件。如果您认为应该编辑您的OP以更改|| 到&&,请这样做。
2020年

Answers:


86

您应该take()从队列继续。您可以使用毒药告诉工人停下来。例如:

private final Object POISON_PILL = new Object();

@Override
public void run() {
    //worker loop keeps taking en element from the queue as long as the producer is still running or as 
    //long as the queue is not empty:
    while(isRunning) {
        System.out.println("Consumer "+Thread.currentThread().getName()+" START");
        try {
            Object queueElement = inputQueue.take();
            if(queueElement == POISON_PILL) {
                 inputQueue.add(POISON_PILL);//notify other threads to stop
                 return;
            }
            //process queueElement
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

//this is used to signal from the main thread that he producer has finished adding stuff to the queue
public void finish() {
    //you can also clear here if you wanted
    isRunning = false;
    inputQueue.add(POISON_PILL);
}

42
我的技术词典+1刚有了“毒丸”一词丰富
Kiril 2012年

2
我已经尝试过了,并且工作正常,除了一句修改:我必须做inputQueue.put(POISON_PILL); 不提供,因为如果我做到了offer()且此时队列已满(即工人真的很懒),它将不会在其中添加POISON PILL元素。请问这是正确的,还是我在说愚蠢的话?
Shivan Dragon 2012年

@AndreiBodnarescu你说的没错。我以为我得到了所有这些,但只有一个固定的:)
John Vint 2012年

@AndreiBodnarescu另外,如果您尝试过此方法并提出问题,我认为这还不够吗?
John Vint 2012年

@JohnVint:嗯,您刚刚对答案做了更多修改,然后我认为是必要的。由于每个工作人员都会通过finish()调用添加毒药,因此,一旦偶然发现它,您就无需在主循环中重新添加该毒药,可以将其消耗掉,其他工作人员仍然可以使用它毒药。我在这里使用的方法也是“ put”,而不是“ add”或“ offer”。
Shivan Dragon

14

我会给工人一个特殊的工作包,以告知他们应该关闭:

public class ConsumerWorker implements Runnable{

private static final Produced DONE = new Produced();

private BlockingQueue<Produced> inputQueue;

public ConsumerWorker(BlockingQueue<Produced> inputQueue) {
    this.inputQueue = inputQueue;
}

@Override
public void run() {
    for (;;) {
        try {
            Produced item = inputQueue.take();
            if (item == DONE) {
                inputQueue.add(item); // keep in the queue so all workers stop
                break;
            }
            // process `item`
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

}

要停止工作人员,只需添加ConsumerWorker.DONE到队列中即可。


1

在您尝试从队列中检索元素的代码块中,使用poll(time,unit)代替take()

try { 
    Object queueElement = inputQueue.poll(timeout,unit);
     //process queueElement        
 } catch (InterruptedException e) {
        if(!isRunning && queue.isEmpty())
         return ; 
 } 

通过指定适当的timeout值,可以确保在出现不幸的序列时线程不会继续阻塞

  1. isRunning 是真的
  2. 队列为空,因此线程进入阻塞的等待状态(如果使用 take()
  3. isRunning 设置为false

是的,这是我尝试过的替代方法之一,但是它也有一些缺点:首先,我对poll()进行了许多不必要的调用,然后执行此操作(!isRunning && queue.isEmpty())并从队列中取出内容必须使用同步块将它们全部同步,这是多余的,因为BlockingQueue已经自己完成了所有工作。
Shivan Dragon 2012年

为了避免第一点,为什么不安排将设置isRunning为false的线程在等待take()调用的线程上发送中断?catch块仍然以同样的方式-这并不需要单独的同步-除非你打算设置isRunning从假回真正的..
巴斯卡尔

我也尝试过,向您的线程发送中断。问题是,如果您的线程是由ExecutorService启动的(例如FixedThreadPool),则当您执行executorService.shutDown()时,所有线程都将收到InterruptedException,这将使它们在任务中途停止(因为现在已将它们进行处理) InterruptedException作为停止器)。此外,通过这样抛出的异常进行通信不是很有效。
Shivan Dragon 2012年

1

我们不能使用来做到这一点CountDownLatch,其中大小是生产者中的记录数。每个消费者都将countDown在处理记录之后。awaits()当所有任务完成时,它越过方法。然后停止所有您的消费者。由于所有记录均已处理。


0

您可以使用多种策略,但是一个简单的策略就是拥有一个任务子类,该子类表示工作已结束。生产者不直接发送此信号。而是,它使该任务子类的实例入队。当您的使用者之一执行此任务并执行该任务时,将导致发送信号。


0

我不得不使用多线程生产者和多线程使用者。我最终提出了一个Scheduler -- N Producers -- M Consumers方案,每两个通过一个队列进行通信(总共两个队列)。调度程序用产生数据的请求填充第一个队列,然后用N个“毒丸”填充它。有一个活跃生产者的计数器(atomic int),最后接收到最后一个毒药的生产者将M个毒药发送到消费者队列。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.