NomNuggetNom
NomNuggetNom

Reputation: 19

CompletableFuture showing inconsistent threading behavior

CompletableFuture's documentation specifies the following behavior for asynchronous execution:

All async methods without an explicit Executor argument are performed using the ForkJoinPool#commonPool() (unless it does not support a parallelism level of at least two, in which case, a new Thread is created to run each task). To simplify monitoring, debugging, and tracking, all generated asynchronous tasks are instances of the marker interface AsynchronousCompletionTask.

However, the behavior of synchronous (or at least the non async) methods remain unclear. In most situations, the code executes with the original thread, like so:

Thread thread = Thread.currentThread();
CompletableFuture.runAsync(() -> {
        // Should run on the common pool - working as expected
        assert thread != Thread.currentThread();
}).thenRun(() -> {
        // Returns to running on the thread that launched.. sometimes?
        assert thread == Thread.currentThread();
}).join();

However, repetition of this test yields inconsistent results, as the second block of code sometimes uses the common pool instead. What is the intended behavior in this situation?

Upvotes: 1

Views: 642

Answers (1)

Evan Geng
Evan Geng

Reputation: 81

After looking at the OpenJDK implementation of CompletableFuture here, it smells like you're encountering a sort of race condition. Suppose we're doing something along the lines of runAsync(a).thenRun(b). If a finishes execution on the common pool thread before thenRun(b) gets called on the current thread, then b is run immediately on the current thread once thenRun(b) finally gets called. On the other hand, if thenRun(b) is called before a finishes on the other thread, then b is run on the same thread as a once a finally finishes.

It's sort of like this:

class CompletableFuture:
  function runAsync(a):
    function c():
      run a
      for each b in the handler stack:
        run b
    run c on a different thread
    return future representing c
  function thenRun(b):
    if c is done:
      run b
    else:
      put b in c's handler stack

Clearly, b is run in c's thread if thenRun is called before a finishes. Otherwise, b is run in the current thread.

If you want more consistent behaviour in terms of which thread runs what, you should try using CompletableFuture#thenRunAsync, which guarantees the handler is executed in some particular executor pool.

Upvotes: 2

Related Questions