【问题标题】:Blocking queue and multi-threaded consumer, how to know when to stop阻塞队列和多线程消费者,如何知道何时停止
【发布时间】:2012-01-23 16:05:57
【问题描述】:

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

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

每个这样的 Worker 都会在队列上做一个take() 并处理任务。

我的问题是,让 Worker 知道什么时候没有更多工作要做的最佳方式是什么。换句话说,我如何告诉Workers生产者已经完成加入队列,并且从这一点开始,每个worker看到队列为空时应该停止。

我现在得到的是一个设置,我的 Producer 用一个回调初始化,当他完成它的工作(向队列中添加东西)时触发该回调。我还保留了我创建并提交到 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 会在消耗队列中的所有内容之前停止。

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

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

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 的评论而更新。

感谢您的关注。你是对的,这个问题中的第一个 sn-p 代码有(以及其他问题)while(isRunning || !inputQueue.isEmpty()) 没有真正意义的那个。

在我的实际最终实现中,我做了一些更接近您替换“||”的建议(or) with "&&" (and),意思是每个工人(消费者)现在只检查他从列表中得到的元素是否是毒丸,如果是的话就停止(所以理论上我们可以说工人有正在运行且队列不能为空)。

【问题讨论】:

  • 一个 executorService 已经有一个队列,所以你不需要另一个。您可以使用 shutdown() 启动整个 executor 服务。
  • @PeterLawrey 很抱歉,我不明白您的评论...
  • 由于 ExecutorService 已经有一个队列,你可以只向它添加任务,你不需要额外的队列,也不需要弄清楚如何停止它们,因为这已经实现了。
  • 没错,但我想避免处理所有那些 Callable 和 Runnable 对象,我只想要一个包含我的实际业务数据的队列。但是,我将检查这样做是否不会导致更快的实现,然后如果我要使用阻塞队列来处理它们。
  • @ShivanDragon:我提出了一个链接question,人们暗示你的OP应该有条件&&而不是||。如果您觉得应该编辑您的 OP 以更改 ||到 &&,请这样做。

标签: java multithreading producer-consumer blockingqueue


【解决方案1】:

您应该从队列中继续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);
}

【讨论】:

  • +1 我的科技词典刚刚用“毒丸”丰富了一个词
  • 我已经试过了,它工作正常,除了一个小修改:我必须做 inputQueue.put(POISON_PILL);不提供,因为如果我提供()并且当时队列已满负荷(即工人真的很懒),它不会向其中添加毒丸元素。请问这是正确的,还是我在说傻话?
  • @AndreiBodnarescu 你说得对,它不提供。我以为我得到了所有但只修复了一个:)
  • @AndreiBodnarescu 另外,如果您尝试过这个并提出问题,我假设这还不够?
  • @JohnVint:好吧,你刚刚对你的答案做了更多的修改,我认为这是必要的。由于每个worker都会通过finish()调用添加一个毒丸,一旦你偶然发现毒丸,你不需要在主循环中重新添加毒丸,可以食用它,其他worker仍然会拥有每个它的毒丸。此外,我在这里使用的方法是 "put" ,而不是 "add" 或 "offer"。
【解决方案2】:

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

【讨论】:

    【解决方案3】:

    我会向工人发送一个特殊的工作包,表示他们应该关闭:

    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 添加到队列中即可。

    【讨论】:

      【解决方案4】:

      在您尝试从队列中检索元素的代码块中,使用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 已经自己处理了所有这些。
      • 为了避免第一点,为什么不直接安排将isRunning 设置为false 的线程在等待take() 调用的线程上发送中断? catch 块仍然以相同的方式工作 - 这不需要单独同步 - 除非您计划将 isRunning 从 false 设置回 true..
      • 我也尝试过,向您的线程发送中断。问题是,如果您的线程是由 ExecutorService(如 FixedThreadPool)启动的,那么当您执行 executorService.shutDown() 时,您的所有线程都将收到 InterruptedException 这将使它们在任务中途停止(因为它们现在被操纵来处理InterruptedException 作为一个停止器)。此外,通过这样的抛出异常进行通信并不是很有效。
      【解决方案5】:

      您可以使用多种策略,但一个简单的策略是拥有一个任务子类来表示工作的结束。生产者不直接发送此信号。相反,它将这个任务子类的一个实例排入队列。当您的一个消费者完成此任务并执行它时,就会发送信号。

      【讨论】:

        【解决方案6】:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2015-09-25
          • 1970-01-01
          • 1970-01-01
          • 2011-01-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多