Thread-safe FIFO queue with unique items and thread pool - java

I have to manage scheduled file replications in a system. The file replications are scheduled by users and I need to restrict the amount of system resources used during replication. The amount of time that each replication may take is not defined (i.e. a replication may be scheduled to run every 15 minutes and the previous run may still be running when the next run is due) and a replication should not be queued if it's already queued or running.
I have a scheduler that periodically checks for due file replications and, for each one, (1) add it to a blocking queue if it is not queued nor running or (2) drop it otherwise.
private final Object scheduledReplicationsLock = new Object();
private final BlockingQueue<Replication> replicationQueue = new LinkedBlockingQueue<>();
private final Set<Long> queuedReplicationIds = new HashSet<>();
private final Set<Long> runningReplicationIds = new HashSet<>();
public boolean add(Replication replication) {
synchronized (scheduledReplicationsLock) {
// If the replication job is either still executing or is already queued, do not add it.
if (queuedReplicationIds.contains(replication.id) || runningReplicationIds.contains(replication.id)) {
return false;
}
replicationQueue.add(replication)
queuedReplicationIds.add(replication.id);
}
I also have a pool of threads that waits until there is a replication in the queue and executes it. Below is the main method of each thread in the thread pool:
public void run() {
while (True) {
Replication replication = null;
synchronized (scheduledReplicationsLock) {
// This will block until a replication job is ready to be run or the current thread is interrupted.
replication = replicationQueue.take();
// Move the ID value out of the queued set and into the active set
Long replicationId = replication.getId();
queuedReplicationIds.remove(replicationId);
runningReplicationIds.add(replicationId);
}
executeReplication(replication)
}
}
This code gets into a deadlock because the first thread in the thread poll will get scheduledLock and prevent the scheduler to add replications to the queue. Moving replicationQueue.take() out of the synchronized block will eliminate the deadlock, but then it's possible that a element is removed from the queue and the hash sets are not atomically updated with it, which could cause a replication to be incorrectly dropped.
Should I use BlockingQueue.poll() and release the lock + sleep if the queue is empty instead of using BlockingQueue.take() ?
Fixes to the current solution or other solutions that meet the requirements are welcome.

wait / notify
Keeping your same control flow, instead of blocking on the BlockingQueue instance while holding the mutex lock, you can wait on notifications for the scheduledReplicationsLock forcing the worker thread to release the lock and return to the waiting pool.
Here down a reduced sample of your producer:
private final List<Replication> replicationQueue = new LinkedList<>();
private final Set<Long> runningReplicationIds = new HashSet<>();
public boolean add(Replication replication) {
synchronized (replicationQueue) {
// If the replication job is either still executing or is already queued, do not add it.
if (replicationQueue.contains(replication) || runningReplicationIds.contains(replication.id)) {
return false;
} else {
replicationQueue.add(replication);
replicationQueue.notifyAll();
}
}
}
The worker Runnable would then be updated as follows:
public void run() {
synchronized (replicationQueue) {
while (true) {
if (replicationQueue.isEmpty()) {
scheduledReplicationsLock.wait();
}
if (!replicationQueue.isEmpty()) {
Replication replication = replicationQueue.poll();
runningReplicationIds.add(replication.getId())
executeReplication(replication);
}
}
}
}
BlockingQueue
Generally you are better off using the BlockingQueue to coordinate your producer and replicating worker pool.
The BlockingQueue is, as the name implies, blocking by nature and will cause the calling thread to block only if items cannot be pulled / pushed from / to the queue.
Meanwhile, note that you will have to update your running / enqueued state management as you will only synchronizing on the BlockingQueue items dropping any constraints. This then will depend on the context, whether this would be acceptable or not.
This way, you would drop all other used mutex(es) and use on the BlockingQueue as your synchronization state:
private final BlockingQueue<Replication> replicationQueue = new LinkedBlockingQueue<>();
public boolean add(Replication replication) {
// not sure if this is the proper invariant to check as at some point the replication would be neither queued nor running while still have been processed
if (replicationQueue.contains(replication)) {
return false;
}
// use `put` instead of `add` as this will block waiting for free space
replicationQueue.put(replication);
return true;
}
The workers will then take indefinitely from the BlockingQueue:
public void run() {
while (true) {
Replication replication = replicationQueue.take();
executeReplication(replication);
}
}

You no need to use any additional synchronization block if you using BlockingQueue
Quote from docs (https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/BlockingQueue.html)
BlockingQueue implementations are thread-safe. All queuing methods achieve their effects atomically using internal locks or other forms of concurrency control.
just use something like this
public void run() {
try {
while (replicationQueue.take()) { //Thread will be wait for the next element in the queue
Long replicationId = replication.getId();
queuedReplicationIds.remove(replicationId);
runningReplicationIds.add(replicationId);
executeReplication(replication);
}
} catch (InterruptedException ex) {
//if interrupted while waiting next element
}
}
}
look in javadoc https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/LinkedBlockingQueue.html#take()
Or you can use BlockinQueue.pool() with timeout settings
UPD: After discussion, I extend LinkedBlockingQueue with two ConcurrentHashSets and add method afterTake() to remove processed Replicas. You do not need an additional synchronizations outside the queue. Just put replica in the first thread and take it in another, and call afterTake() when replication finished. You need to override other method if you want to use it.
package ru.everytag;
import io.vertx.core.impl.ConcurrentHashSet;
import java.util.concurrent.LinkedBlockingQueue;
public class TwoPhaseBlockingQueue<E> extends LinkedBlockingQueue<E> {
private ConcurrentHashSet<E> items = new ConcurrentHashSet<>();
private ConcurrentHashSet<E> taken = new ConcurrentHashSet<>();
#Override
public void put(E e) throws InterruptedException {
if (!items.contains(e)) {
items.add(e);
super.put(e);
}
}
public E take() {
E item = take();
taken.add(item);
items.remove(item);
return item;
}
public void afterTake(E e) {
if (taken.contains(e)) {
taken.remove(e);
} else if (items.contains(e)) {
throw new IllegalArgumentException("Element still in the queue");
}
}
}

Related

Shutdown executor without knowing when it will finish all the tasks

I have a task that should wait for a condition (OpenCms startup) and then notify some listeners.
to do this I used an ExecutorService:
public void check(final ExecutorService executorService) {
executorService.submit(() -> {
waitForInitialization();
notifyListeners();
});
}
private void waitForInitialization() {
while (OpenCms.getRunLevel() < OpenCms.RUNLEVEL_4_SERVLET_ACCESS) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
the question is how to shutdown this executor. I could use awaitTermination but I should provide a timeout which I don't know exactly. it could vary from one environment to the other.
the question is how to shutdown this executor. I could use awaitTermination but I should provide a timeout which I don't know exactly. it could vary from one environment to the other.
The question I would ask is do you need a timeout at all? Often if you know that a particular job will finish at some point I just wait for a timeout Long.MAX_VALUE – effectively forever. Other times I'll do something like:
threadPool.shutdown();
threadPool.awaitTermination(...) of some small value (maybe 10 seconds)
threadPool.shutdownNow(); to interrupt the threads
threadPool.awaitTermination(...); of Long.MAX_VALUE because I know the jobs will finish eventually
it could vary from one environment to the other.
If it could vary then maybe you should be able to calculate what a proper timeout would be for each environment?
Lastly, don't be afraid of passing in a ThreadFactory that creates daemon threads. For some jobs I shutdown() the thread-pool but never wait for them to complete because I don't care about their status so I create the threads in the pool with daemon enabled maybe using something like the following thread-factory.
/** Thread factory which sets name and optionally daemon */
public class PoolNameThreadFactory implements ThreadFactory {
private final String poolName;
private final Boolean daemon;
private final AtomicInteger threadNum = new AtomicInteger(0);
public PoolNameThreadFactory(String poolName) {
this(poolName, null);
}
public PoolNameThreadFactory(String poolName, boolean daemon) {
this(poolName, (Boolean) daemon);
}
private PoolNameThreadFactory(String poolName, Boolean daemon) {
this.poolName = poolName;
this.daemon = daemon;
}
#Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName(poolName + '-' + threadNum.incrementAndGet());
if (daemon != null) {
thread.setDaemon(daemon);
}
return thread;
}
}
Based on the provided information, I'd clearly recommend an event-based approach. Especially knowing that in your own code there is a call like notifyListeners(). In fact, that's the way to go.
In summary, once the precondition is met somewhere in your app, just notify the listeners of this event. In your example, the "OpenCms run level" change is typically an event. So, just go for an Observer pattern, or a pub-sub model to observe or monitor these changes.
If you modify your approach, you will not have to worry about the waiting time around the initialization, except if you wish to handle the absence of event specifically. That would be done again after some timeout, but with the advantage of not blocking an executor thread.

How to release a semaphore and let any threads continue?

I want to create a semaphore that prevents a certain method to be executed more than 1x at a time.
If any other thread requests access, it should wait until the semaphore is released:
private Map<String, Semaphore> map;
public void test() {
String hash; //prevent to run the long running method with the same hash concurrently
if (map.contains(hash)) {
map.get(hash).aquire(); //wait for release of the lock
callLongRunningMethod();
} else {
Semaphore s = new Semaphore(1);
map.put(hash, s);
callLongRunningMethod();
s.release(); //any number of registered threads should continue
map.remove(hash);
}
}
Question: how can I lock the semaphore with just one thread, but release it so that any number of threads can continue as soon as released?
Some clarifications:
Imagine the long running method is a transactional method. Looks into the database. If no entry is found, a heavy XML request is send and persisted to db. Also maybe further async processed might be triggered as this is supposed to be the "initial fetch" of the data. Then return the object from DB (within that method). If the DB entry had existed, it would directly return the entity.
Now if multiple threads access the long running method at the same time, all methods would fetch the heavy XML (traffic, performance), and all of them would try to persist the same object into the DB (because the long running method is transactional). Causing eg non-unique exceptions. Plus all of them triggering the optional async threads.
When all but one thread is locked, only the first is responsible for persisting the object. Then, when finished, all other threads will detect that the entry already exists in DB and just serve that object.
As far as I understand, you don't need to use Semaphore here. Instead, you should use ReentrantReadWriteLock. Additionally, the test method is not thread safe.
The sample below is the implementation of your logic using RWL
private ConcurrentMap<String, ReadWriteLock> map = null;
void test() {
String hash = null;
ReadWriteLock rwl = new ReentrantReadWriteLock(false);
ReadWriteLock lock = map.putIfAbsent(hash, rwl);
if (lock == null) {
lock = rwl;
}
if (lock.writeLock().tryLock()) {
try {
compute();
map.remove(hash);
} finally {
lock.writeLock().unlock();
}
} else {
lock.readLock().lock();
try {
compute();
} finally {
lock.readLock().unlock();
}
}
}
In this code, the first successful thread would acquire WriteLock while other Threads would wait for release of write lock. After release of a WriteLock all Threads waiting for release would proceed concurrently.
As far as I understand your need you want to be able to ensure that the task is executed by one single thread for the first time then you want to allow several threads to execute it if so you need to rely on a CountDownLatch as next:
Here is how it could be implemented with CountDownLatch:
private final ConcurrentMap<String, CountDownLatch> map = new ConcurrentHashMap<>();
public void test(String hash) {
final CountDownLatch latch = new CountDownLatch(1);
final CountDownLatch previous = map.putIfAbsent(hash, latch);
if (previous == null) {
try {
callLongRunningMethod();
} finally {
map.remove(hash, latch);
latch.countDown();
}
} else {
try {
previous.await();
callLongRunningMethod();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
I think you could do that by using a very high permit number (higher than the number of threads, e.g. 2000000).
Then in the function that should run exclusively you acquire the complete number of permits (acquire(2000000)) and in the other threads you acquire only a single permit.
I think that the easiest way to do this would be using an ExecutorService and Future:
class ContainingClass {
private final ConcurrentHashMap<String, Future<?>> pending =
new ConcurrentHashMap<>();
private final ExecutorService executor;
ContainingClass(ExecutorService executor) {
this.executor = executor;
}
void test(String hash) {
Future<?> future = pending.computeIfAbsent(
hash,
() -> executor.submit(() -> longRunningMethod()));
// Exception handling omitted for clarity.
try {
future.get(); // Block until LRM has finished.
} finally {
// Always remove: in case of exception, this allows
// the value to be computed again.
pending.values().remove(future);
}
}
}
Ideone Demo
Removing the future from the values is thread safe because computeIfAbsent and remove are atomic: either the computeIfAbsent is run before the remove, in which case the existing future is returned, and is immediately complete; or it is run after, and a new future is added, resulting in a new call to longRunningMethod.
Note that it removes the future from pending.values(), not from the pending directly: consider the following example:
Thread 1 and Thread 2 are run concurrently
Thread 1 completes, and removes the value.
Thread 3 is run, adding a new future to the map
Thread 2 completes, and tries to remove the value.
If the future were removed from the map by key, Thread 2 would remove Thread 3's future, which is a different instance from Thread 2's future.
This simplifies the longRunningMethod too, since it is no longer required to do the "check if I need to do anything" for the blocked threads: that the Future.get() has completed successfully in the blocking thread is sufficient to indicate that no additional work is needed.
I ended as follows using CountDownLatch:
private final ConcurrentMap<String, CountDownLatch> map = new ConcurrentHashMap<>();
public void run() {
boolean active = false;
CountDownLatch count = null;
try {
if (map.containsKey(hash)) {
count = map.get(hash);
count.await(60, TimeUnit.SECONDS); //wait for release or timeout
} else {
count = new CountDownLatch(1);
map.put(hash, count); //block any threads with same hash
active = true;
}
return runLongRunningTask();
} finally {
if (active) {
count.countDown(); //release
map.remove(hash, count);
}
}
}

Java - Multiple queue producer consumer

I've got the following code:
while(!currentBoard.boardIsValid()){
for (QueueLocation location : QueueLocation.values()){
while(!inbox.isEmpty(location)){
Cell c = inbox.dequeue(location);
notifyNeighbours(c.x, c.y, c.getCurrentState(),previousBoard);
}
}
}
I've got a consumer with a few queues (all of their methods are synchronised). One queue for each producer. The consumer loops over all the queues and checks if they've got a task for him to consume.
If the queue he's checking has a task in it, he consumes it. Otherwise, he goes to the check the next queue until he finishes iterating over all the queues.
As of now, if he iterates over all the queues and they're all empty, he keeps on looping rather than waiting for one of them to contain something (as seen by the outer while).
How can I make the consumer wait until one of the queues has something in it?
I'm having an issue with the following scenario: Lets say there are only 2 queues. The consumer checked the first one and it was empty. Just as he's checking the second one (which is also empty), the producer put something in the first queue. As far as the consumer is concerned, the queues are both empty and so he should wait (even though one of them isn't empty anymore and he should continue looping).
Edit:
One last thing. This is an exercise for me. I'm trying to implement the synchronisation myself. So if any of the java libraries have a solution that implements this I'm not interested in it. I'm trying to understand how I can implement this.
#Abe was close. I would use signal and wait - use the Object class built-ins as they are the lightest weight.
Object sync = new Object(); // Can use an existing object if there's an appropriate one
// On submit to queue
synchronized ( sync ) {
queue.add(...); // Must be inside to avoid a race condition
sync.notifyAll();
}
// On check for work in queue
synchronized ( sync ) {
item = null;
while ( item == null ) {
// Need to check all of the queues - if there will be a large number, this will be slow,
// and slow critical sections (synchronized blocks) are very bad for performance
item = getNextQueueItem();
if ( item == null ) {
sync.wait();
}
}
}
Note that sync.wait releases the lock on sync until the notify - and the lock on sync is required to successfully call the wait method (it's a reminder to the programmer that some type of critical section is really needed for this to work reliably).
By the way, I would recommend a queue dedicated to the consumer (or group of consumers) rather than a queue dedicated to the producer, if feasible. It will simplify the solution.
If you want to block across multiple queues, then one option is to use java's Lock and Condition objects and then use the signal method.
So whenever the producer has data, it should invoke the signallAll.
Lock fileLock = new ReentrantLock();
Condition condition = fileLock.newCondition();
...
// producer has to signal
condition.signalAll();
...
// consumer has to await.
condition.await();
This way only when the signal is provided will the consumer go and check the queues.
I solved a similar situation along the lines of what #Abe suggests, but settled on using a Semaphore in combination with an AtomicBoolean and called it a BinarySemaphore. It does require the producers to be modified so that they signal when there is something to do.
Below the code for the BinarySemaphore and a general idea of what the consumer work-loop should look like:
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class MultipleProdOneConsumer {
BinarySemaphore workAvailable = new BinarySemaphore();
class Consumer {
volatile boolean stop;
void loop() {
while (!stop) {
doWork();
if (!workAvailable.tryAcquire()) {
// waiting for work
try {
workAvailable.acquire();
} catch (InterruptedException e) {
if (!stop) {
// log error
}
}
}
}
}
void doWork() {}
void stopWork() {
stop = true;
workAvailable.release();
}
}
class Producer {
/* Must be called after work is added to the queue/made available. */
void signalSomethingToDo() {
workAvailable.release();
}
}
class BinarySemaphore {
private final AtomicBoolean havePermit = new AtomicBoolean();
private final Semaphore sync;
public BinarySemaphore() {
this(false);
}
public BinarySemaphore(boolean fair) {
sync = new Semaphore(0, fair);
}
public boolean release() {
boolean released = havePermit.compareAndSet(false, true);
if (released) {
sync.release();
}
return released;
}
public boolean tryAcquire() {
boolean acquired = sync.tryAcquire();
if (acquired) {
havePermit.set(false);
}
return acquired;
}
public boolean tryAcquire(long timeout, TimeUnit tunit) throws InterruptedException {
boolean acquired = sync.tryAcquire(timeout, tunit);
if (acquired) {
havePermit.set(false);
}
return acquired;
}
public void acquire() throws InterruptedException {
sync.acquire();
havePermit.set(false);
}
public void acquireUninterruptibly() {
sync.acquireUninterruptibly();
havePermit.set(false);
}
}
}

Java Producer Consumer Pattern with Consumer completion notification

I want to implement a producer / consumer scenario where i have multiple producers and a single consumer. Producers keep adding items to a queue and consumer dequeues the items. When the consumer has processed enough items, both the producers and consumer should stop execution. Consumer can easily terminate itself when it process enough items. But the producers should also know when to exit. The typical producer poison pills do not work here.
One way to do it would be to have a shared boolean variable between consumer and producers. Consumer sets the boolean variable to true and producers periodically check the variable and exit if it set to true.
Any better ideas on how i can do this ?
I suppose you can have a shared counter and have a max. If an increment is greater than the max value then the thread cannot add to the queue.
private final AtomicInteger count = new AtomicInteger(0);
private final int MAX = ...;/
private final BlockingQueue<T> queue = ...;
public boolean add(T t){
if(count.incrementAndGet() > MAX)
return false;
return queue.offer(t);
}
Not sure if this approach would be any use.
Include a reference to the producer in the message.
Producer provides a call back method to tell them to stop producing.
Consumer keeps a registry of producers based on the unique set of references
that are passed to it.
When the consumer has had enough, it iterates over the registry of producers, and tells them to stop by calling the callback method.
Would only work if producer and consumer are in the same JVM
Wouldn't stop any new producers from starting up
And I'm not sure it maintains the separation of producer and consumer
Alternatively, as the Queue is the shared resource between these two objects, could you introduce an "isOpen" state on the queue which is checked before the producer writes to it and is set by the consumer when it has done as much work as it is happy to do?
From what I understand you'll need something like this:
private static final BlockingQueue<String> queue = new LinkedBlockingQueue<String>();
private static boolean needMore = true;
static class Consumer implements Runnable
{
Scanner scanner = new Scanner(System.in);
#Override
public void run()
{
do
{
try
{
String s = queue.take();
System.out.println("Got " + s);
needMore = scanner.nextBoolean();
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
while (needMore);
}
}
static class Producer implements Runnable
{
Random rand = new Random();
#Override
public void run()
{
System.out.println("Starting new producer...");
do
{
queue.add(String.valueOf(rand.nextInt()));
try
{
Thread.sleep(1000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
while (needMore);
System.out.println("Producer shuts down.");
}
}
public static void main(String[] args) throws Exception
{
Thread producer1 = new Thread(new Producer());
Thread producer2 = new Thread(new Producer());
Thread producer3 = new Thread(new Producer());
Thread consumer = new Thread(new Consumer());
producer1.start();
producer2.start();
producer3.start();
consumer.start();
producer1.join();
producer2.join();
producer3.join();
consumer.join();
return;
}
The consumer dynamically decides if it needs more data and stops when it has found what it was searching for example; this is simulated by the user inputting true/false for continuing/stopping.
Here is an I/O sample:
Starting new producer...
Starting new producer...
Starting new producer...
Got -1782802247
true
Got 314306979
true
Got -1787470224
true
Got 1035850909
false
Producer shuts down.
Producer shuts down.
Producer shuts down.
This may not look clean on first sight, but I think it's actually cleaner than having an extra variable etc. if you are trying to do this as a part of shutdown process.
Make your consumers an ExecutorService, and from your consumer task, call shutdownNow() when the task decides that the consumers had consumed enough. This will cancel all pending tasks on the queue, interrupt currently running tasks and the producers will start to get RejectedExecutionException upon submission. You can treat this exception as a signal from the consumers.
Only caveat is that when you have multiple consumers, calling shutdownNow() in a serial manner will not guarantee that no task will be executed after one consumer decided it was enough. I'm assuming that's fine. If you need this guarantee, then you can indeed share an AtomicBoolean and let all producers and consumers check it.

Queue with notifications on isEmpty() changes

I have an BlockingQueue<Runnable>(taken from ScheduledThreadPoolExecutor) in producer-consumer environment. There is one thread adding tasks to the queue, and a thread pool executing them.
I need notifications on two events:
First item added to empty queue
Last item removed from queue
Notification = writing a message to database.
Is there any sensible way to implement that?
A simple and naïve approach would be to decorate your BlockingQueue with an implementation that simply checks the underlying queue and then posts a task to do the notification.
NotifyingQueue<T> extends ForwardingBlockingQueue<T> implements BlockingQueue<T> {
private final Notifier notifier; // injected not null
…
#Override public void put(T element) {
if (getDelegate().isEmpty()) {
notifier.notEmptyAnymore();
}
super.put(element);
}
#Override public T poll() {
final T result = super.poll();
if ((result != null) && getDelegate().isEmpty())
notifier.nowEmpty();
}
… etc
}
This approach though has a couple of problems. While the empty -> notEmpty is pretty straightforward – particularly for a single producer case, it would be easy for two consumers to run concurrently and both see the queue go from non-empty -> empty.
If though, all you want is to be notified that the queue became empty at some time, then this will be enough as long as your notifier is your state machine, tracking emptiness and non-emptiness and notifying when it changes from one to the other:
AtomicStateNotifier implements Notifier {
private final AtomicBoolean empty = new AtomicBoolean(true); // assume it starts empty
private final Notifier delegate; // injected not null
public void notEmptyAnymore() {
if (empty.get() && empty.compareAndSet(true, false))
delegate.notEmptyAnymore();
}
public void nowEmpty() {
if (!empty.get() && empty.compareAndSet(false, true))
delegate.nowEmpty();
}
}
This is now a thread-safe guard around an actual Notifier implementation that perhaps posts tasks to an Executor to asynchronously write the events to the database.
The design is most likely flawed but you can do it relatively simple:
You have a single thread adding, so you can check before adding. i.e. pool.getQueue().isEmpty() - w/ one producer, this is safe.
Last item removed cannot be guaranteed but you can override beforeExecute and check the queue again. Possibly w/ a small timeout after isEmpty() returns true. Probably the code below will be better off executed in afterExecute instead.
protected void beforeExecute(Thread t, Runnable r) {
if (getQueue().isEmpty()){
try{
Runnable r = getQueue().poll(200, TimeUnit.MILLISECONDS);
if (r!=null){
execute(r);
} else{
//last message - or on after execute by Setting a threadLocal and check it there
//alternatively you may need to do so ONLY in after execute, depending on your needs
}
}catch(InterruptedException _ie){
Thread.currentThread().interrupt();
}
}
}
sometime like that
I can explain why doing notifications w/ the queue itself won't work well: imagine you add a task to be executed by the pool, the task is scheduled immediately, the queue is empty again and you will need notification.

Categories