我有N个工作人员共享要计算的元素队列。在每次迭代中,每个worker都会从队列中删除一个元素,并且可以生成更多要计算的元素,这些元素将被放在同一个队列中。基本上,每个生产者也是消费者。当队列中没有元素并且所有工作者已经完成计算当前元素时,计算结束(因此不能再生成要计算的元素)。我想避免调度员/协调员,所以工人应该协调。允许工人查明暂停条件是否有效的最佳模式是什么,因此代表其他人暂停计算?
例如,如果所有线程都只执行此循环,那么当所有元素都被计算出来时,它将导致所有线程被永久阻塞:
while (true) {
element = queue.poll();
newElements[] = compute(element);
if (newElements.length > 0) {
queue.addAll(newElements);
}
}
答案 0 :(得分:6)
保持活动线程的数量。
public class ThreadCounter {
public static final AtomicInteger threadCounter = new AtomicInteger(N);
public static final AtomicInteger queueCounter = new AtomicInteger(0);
public static final Object poisonPill = new Object();
public static volatile boolean cancel = false; // or use a final AomticBoolean instead
}
您的线程的轮询循环应如下所示(我假设您使用的是BlockingQueue
)
while(!ThreadCounter.cancel) {
int threadCount = ThreadCounter.threadCounter.decrementAndGet(); // decrement before blocking
if(threadCount == 0 && ThreadCounter.queueCounter.get() == 0) {
ThreadCounter.cancel = true;
queue.offer(ThreadCounter.poisonPill);
} else {
Object obj = queue.take();
ThreadCounter.threadCounter.incrementAndGet(); // increment when the thread is no longer blocking
ThreadCounter.queueCounter.decrementAndGet();
if(obj == ThreadCounter.poisonPill) {
queue.offer(obj); // send the poison pill back through the queue so the other threads can read it
continue;
}
}
}
如果一个线程即将在BlockingQueue
上阻塞,那么它会递减计数器;如果所有线程都在等待队列(意味着counter == 0
),那么最后一个线程将cancel
设置为true,然后通过队列发送毒丸以唤醒其他线程;每个线程看到毒丸,通过队列将其发回以唤醒剩余的线程,然后在看到cancel
设置为true时退出循环。
编辑我通过添加queueCounter
删除了数据竞争,该queueCounter.incrementAndGet()
维护了队列中对象数量的计数(显然,您还需要添加{ {1}}调用您向队列添加对象的任何地方。这样做如下:如果threadCount == 0
,但queueCount != 0
,那么这意味着一个线程刚刚从队列中删除了一个项目但尚未调用threadCount.getAndIncrement
,因此取消变量是不设置为true。 threadCount.getAndIncrement
调用先于queueCount.getAndDecrement
调用,这一点很重要,否则您仍然会进行数据竞争。你调用queueCount.getAndIncrement
的顺序无关紧要,因为你不会将它与threadCount.getAndDecrement
的调用交织在一起(后者将在循环结束时被调用,前者将被调用在循环的开头)。
请注意,您不能仅使用queueCount
来确定何时结束进程,因为线程可能仍处于活动状态而尚未在队列中放置任何数据 - 换句话说,{{1}将为零,但一旦线程完成当前迭代,它将为非零。
您可以改为通过队列发送取消线程(N-1)queueCount
,而不是通过队列重复发送poisonPill
。如果您使用不同的队列使用此方法,请务必谨慎,因为某些队列(例如亚马逊的简单队列服务)可能会返回相当于poisonPills
方法的多个项目,在这种情况下您需要重复发送take
以确保一切都关闭。
此外,代替使用poisonPill
循环,您可以使用while(!cancel)
循环并在循环检测到while(true)