线程池,其中工作者既是生产者又是消费者

时间:2012-10-22 06:24:39

标签: java concurrency producer-consumer java.util.concurrent worker

我有一个无限制的作业队列,可以异步处理。每个作业的处理可能会也可能不会触发为此队列创建新作业。

我想要一个包含几个工作线程的池来从这个队列中获取项目并并行处理它们,直到队列都为空 所有工作线程都空闲等待队列中的新作业(因为繁忙的工作人员最终可能会在队列中添加新作业)。

是否有使用java.util.concurrent实现的方法,我可以用它来解决这个特殊的问题,工人也是生产者?目前尚不清楚API是否以直接的方式支持这种情况。

特别是,我希望能够检测终止条件,即,当没有更多可用作业(空作业队列)时,将不再生成作业(所有空闲工作线程)。

修改

Nam San的回答似乎是最优雅的方法,基本上归结为跟踪提交的工作数量与已完成工作的数量,并使用这些数字等于终止的情况条件。

我使用扩展java.util.concurrent的{​​{1}}实现实现了一个完整的示例,以实现此目的,并专门化作业队列以接受以特定方式排序的ThreadPoolExecutor个实例

  • TestExecutor.java:一个自定义执行程序,它扩展Comparable但有其他方法来执行可能创建新作业的作业,以及一个新的等待方法,等待所有提交的作业完成。
  • WorkUnit.java:可比较的可运行作业的示例,可以创建新作业以提交至ThreadPoolExecutor
  • Test.java:包含使用TestExecutor WorkUnit实例运行示例的主要方法。

5 个答案:

答案 0 :(得分:1)

我不认为消费者也是生产者是相关的,因为在生产者 - 消费者模式中,他们是完全不同的关注点。

您的消费者已经拥有对队列的引用 - 只需让他们像生产者一样添加它。

您可以使用AtomicInteger或类似工具来记录当前有多少工作人员,如果您想等到他们全部静止,可以使用CountDownLatch

答案 1 :(得分:1)

我已经看到了针对此类问题的几种不同解决方案。

一个是仍然使用poll作为主线程中的阻塞调用,就像在你的代码中一样,但是要将一个“虚拟”对象从一个worker中排队,以便在它可能的情况下唤醒主线程永远等待。例如,任何在不向队列添加更多项目的情况下完成的工作者应该提交一个虚拟作业,主线程识别并忽略该作业(它仅用于唤醒主线程)。您可以通过跟踪活动作业的数量来创建更少的虚拟对象,从而减少“虚假唤醒”,代价是一些复杂性 - 只有最后一个作业需要添加一个虚拟对象。

另一种方法是等待另一个对象。例如,任何旧的Object都可以。在此对象上具有主线程wait()。然后,当任务完成时,作业会使用Object.notify()唤醒此线程。同样,通过计数,您可以减少所需通知的数量。

最优雅的解决方案可能是使用Semaphore。基本上,信号量的值将是“飞行中的工作+队列项目”的数量的负数。当作业从队列中挑选一个项目时,该值不会改变(因为在飞行中作业增加1,而队列项目减少一个),但是每个作业应该为他们添加的每个作业调用reducePermits(),并且在他们完成之前调用一个release()。

然后主线程可以在工作期间阻止acquire()。当它醒来时,一切都已完成(因为在飞行中+排队的工作为零)。您将启动另一个线程来实际执行poll调用并添加作业(当前由主线程完成),并且当主线程上的acquire返回时,可以关闭此工作程序。但是,让现有工人poll()自己而不是完成工作可能更简单。那么你根本不需要这个传递函数。

事实上,使用Semaphore解决方案,为什么不完全省略队列,并使用内置于执行程序中的队列?也就是说,让工人通过executor.submit(newJob(nextJob))提交新作品吗?在内部,执行程序线程无论如何都会阻塞阻塞队列,因此在显式外部队列中存在一些重复。

答案 2 :(得分:1)

多年前,我不得不做类似但有限堆叠的事情。我将从你的问题中理解,给出一个可能的解决方案,也许这不是你真正想要的,但也许它会给你一些启发,我希望。

idle_thread = MAX_THREAD;
do
{
if(queue != empty) // If thread have work to do
{
 idle_threads--;  // Count this threads was a worker   
 flag = true;
 while(queue != empty)  // Until queue have work
 {
    synchronized(this)
    {
        // task =  take_out_of_queue;
    }
 }
}
 if(flag) // This flag must to be local to each thread, it is use to insure 
 {        // that threads will count this only one time for each time 
          // the queue got empty
   synchronized(this)
   {
    if(flag == false)
    idle_threads++;  // Count thread as a idle one
    flag = false;
  }
}
if(idle_threads == MAX_THREADS) out = true; // When all threads are idle stop the work loop

}while(!out)

答案 3 :(得分:1)

请参阅Directory Scanner上的帖子,它符合大多数要求。但它没有用Futures和Callable实现。不得不考虑一个。每项任务都没有重要性。没有结果和例外产生。它只是一个平行的&以递归的方式扫描文件。

答案 4 :(得分:1)

下面的代码演示了如何使用Executor周围的包装类来计算已提交作业的数量,并将其与已完成作业的数量进行比较,以实现您的目标。请注意,您的任务必须调用包装类的execute方法,而不是直接调用基础Executor。扩展下面的包装器来包装“提交”应该是微不足道的。如果需要,ExecutorService的方法。

public class ExampleExecutor {

    private final Executor executor;
    private long submitCount = 0;
    private long doneCount = 0;

    public ExampleExecutor(Executor executor) {
        this.executor = executor;
    }

    public synchronized void execute(Collection<Runnable> commands) {
        for (Runnable command : commands) {
            execute(command);
        }
    }

    public synchronized void execute(final Runnable command) {
        submitCount ++;

        executor.execute(new Runnable() {
            public void run() {
                try {
                    command.run();
                } finally {
                    synchronized (ExampleExecutor.this) {
                        doneCount++;
                        if (doneCount == submitCount) {
                            ExampleExecutor.this.notifyAll();
                        }
                    }
                }
            }
        });
    }

    public synchronized void awaitCompletion() throws InterruptedException {
        while (doneCount != submitCount) {
            this.wait();
        }
    }
}

编辑:在下面添加测试用例以演示如何使用上述代码

public class Test {

    static class Task implements Runnable {
        private final String id;
        private final long repetitions;
        private final long respawnSize;
        private final ExampleExecutor executor;

        public Task(String id, long repetitions, long respawnSize, ExampleExecutor executor) {
            this.id = id;
            this.repetitions = repetitions;
            this.respawnSize = respawnSize;
            this.executor = executor;
        }

        public void run() {
            for (int i = 0; i < respawnSize; i ++) {
                // Spawning new sub tasks
                executor.execute(new Task(id + "-" + i, repetitions/2, 0, null));
            }

            double sum = 0;
            for (int i = 0; i < repetitions; i++) {
                sum += Math.sin(i);
            }

            System.err.println(id + " completed at " + System.currentTimeMillis() + " with sum=" + sum);
        }
    }

    public static void main(String argv[]) throws InterruptedException {
        ExampleExecutor executor = new ExampleExecutor(Executors.newFixedThreadPool(2));
        executor.execute(new Task("0", 2000000, 100, executor));

        System.err.println("main thread awaits completion");
        executor.awaitCompletion();
        System.err.println("main thread recieved completion event");
    }
}