- There are a lot of asynchronous activities that must be awaited (to continue processing).
- We do not want to await each asynchronous activity in a separate thread.
Sometimes we have to use a third-party library with asynchronous methods returning java.util.concurrent.Future instances.
Such an API is inconvenient because the Future does not give an elegant way to await the result. We have to either retrieve
the result synchronously by invoking the Future.get() method, or check periodically the result availability by invoking
the Future.get(long, TimeUnit) method. The first approach eliminates all the advantages of asynchronicity, and both can
leave you with lots of threads in the WAITING/TIMED_WAITING state. If you use a thread pool, it will eventually be exhausted.
All depends on how many such asynchronous tasks are awaited in parallel. Nonetheless, lots of awaiting threads means
suboptimal use of resources (CPU/RAM).
The AwaitableQueue class provides an efficient
approach to await completion of asynchronous tasks that do not provide a convenient way to await such as the CompletableFuture class.
Each asynchronous activity must implement the IAwaitable interface
and be enqueued with either the enqueue(IAwaitable) or enqueue(IAwaitable, Duration) methods. These methods return
an instance of the CompletableFuture class which can be used for asynchronous awaiting without any downsides.
The provided future can be safely interrupted as well, for example, with the CompletableFuture.cancel(boolean) method.
As soon as a task is completed it is dequeued automatically.
The implementation is based on a single-threaded executor that, in an infinite loop, sequentially polls enqueued tasks, giving each a small piece of time to synchronously wait for completion. If a task completes in time, the appropriate future is completed as well and the task is dequeued. Awaiting all tasks in a single thread allows to utilize CPU/RAM effectively - only one thread is in active wait. On the other hand all tasks are polled sequentially thus in case of long queue fast tasks can await completion longer. This negative effect can be minimized by decreasing the task poll timeout or using a separate queue.
In the following example an asynchronous download() method returns a resource content by its URI. The provided instance
of the Future<String> is converted to a CompletableFuture<String> instance that can be awaited efficiently,
for example by using the AsyncPipeline.
AwaitableQueue awaitableQueue = new AwaitableQueue(Asynchronizer.commonPool());
...
Future<String> contentFuture = download("https://...");
IAwaitable<String> contentAwaitable = IAwaitable.of(contentFuture);
CompletableFuture<String> contentCompletableFuture = awaitableQueue.enqueue(contentAwaitable);In this way the AwaitableQueue can be used to convert an obsolete, inconvenient, and awkward API to a modern asynchronous one.
The IAwaitable interface already has the factory method based on a Future instance. Other types of awaitable objects
are also easy to convert to the IAwaitable. For example, you have a CountDownLatch instance and can access some result
as soon as the latch be opened.
// A latch
CountDownLatch resultLatch;
// An accessor to the latched result
Supplier<String> resultSupplier;Add the LatchedResult class that implements the IAwaitable interface.
class LatchedResult<T> implements IAwaitable<T> {
private final CountDownLatch resultLatch;
private final Supplier<T> resultSupplier;
public LatchedResult(CountDownLatch resultLatch, Supplier<T> resultSupplier) {
this.resultLatch = resultLatch;
this.resultSupplier = resultSupplier;
}
@Override
public IAwaiter<T> getAwaiter() {
return new IAwaiter<>() {
@Override
public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
return resultLatch.await(timeout, unit);
}
@Override
public T getResult() {
return resultSupplier.get();
}
};
}
}After that you will be able to create an IAwaitable instance.
IAwaitable<String> latchedAwaitable = new LatchedResult<>(resultLatch, resultSupplier);
CompletableFuture<String> resultCompletableFuture = awaitableQueue.enqueue(latchedAwaitable);