Concepts / Retrieving Results from Futures

Retrieving Results from Futures

A Runnable is handed to an ExecutorService with execute, which returns as soon as the task is placed in a queue.

  • Programming

Why Hand Work to a Thread Pool

Suppose a program has several pieces of work it wants to run in parallel. It is possible to create and manage a separate thread for every piece, but that quickly becomes tedious. The ExecutorService approach separates two responsibilities: the main program describes the work, and a pool of threads carries it out. This article answers a practical question. Once the main program has handed a task to the pool, how does it send the task off, and how does it get back an answer when the task produces one?

Handing a Runnable to execute

A Runnable object describes a unit of work. To give such an object to an ExecutorService, the program calls execute on the executor and passes the task in: executor.execute(task). The call does not run the task on the spot in the calling thread. Instead, the task is placed into a queue of tasks waiting to be run.

The method returns immediately once the task has been queued. The caller moves on to its next statement without waiting. The threads in the pool are the ones that take tasks off the queue and execute them. This split is the heart of the design: the code that submits work and the code that performs work run on different threads, and submission is quick because it only involves enqueueing.

java
Notes on the code
  • The Runnable describes the work but does not run it
  • execute places the task in the queue and returns
  • The next line may run before the task has finished
Main programcaller threadExecutorServiceTask queuePool threadworker threadexecute(task)enqueue taskreturns immediatelytakes taskruns task later
After execute() is called, what runs on the caller's thread and what runs on a worker thread, and when does the caller move on to its next line?

What do you think happens?

Immediately after executor.execute(task) returns, has the task necessarily finished?

  • Yes, because execute waits for the task to complete

  • No, because execute returns once the task is queued

Reveal answer
Answer:

No, because execute returns once the task is queued

execute returns as soon as the task is placed in the queue. The caller then moves on without waiting, and the pool threads take the task off the queue and run it at a later time.

Why a Runnable Cannot Return a Value

A Runnable is fine when a task only performs an action. When a task must return a result, the ExecutorService framework offers a different type: Callable<T>. It is a parameterized functional interface. It declares a single method named call, which takes no parameters and returns a value of type T. The type parameter T names whatever kind of value the task produces, so a Callable<Integer> returns an integer and a Callable<String> returns a string.

FeatureRunnableCallable<T>
PurposeDescribes a unit of actionDescribes a unit of computation
MethodPerforms its work with no value handed backDeclares call, which takes no parameters and returns a value of type T
Value producedNothing is handed backA value of type T, such as an integer or a string
Submitted withexecute or submitsubmit
Caller receivesNo result to retrieveA Future<T> that stands in for the value

Runnable and Callable compared. The return value is the key difference.

The contrast with Runnable is the return value. A Runnable task is a unit of action with nothing handed back, while a Callable task is a unit of computation that yields something. That difference is what makes a separate mechanism for retrieving results necessary.

Submitting a Callable and Receiving a Future

A Callable object c is submitted with executor.submit(c). Like execute, the Callable is not run in the calling thread at the moment of submission. It will be executed at some later time. This creates an obvious question: once the computation completes, how does the program get its answer?

The answer is a second interface, Future<T>. A Future<T> stands in for a value of type T that may not be available yet. The call executor.submit(c) returns such a Future, and the Future represents the result of the computation that is still under way. The program holds onto this placeholder and asks it for the value when it needs the value.

Main programExecutorServicePool threadFuture<T>placeholder for the valuesubmit(c)returns Future<T>queues c for later call()call() result feeds the Futureget() when value is needed
How do the caller, the executor, and the worker thread interact when submit() is called, and what object does the caller get back before the task has finished?

The diagram shows two paths that start at the moment of submission. One path leads from the main program to the Future, which the main program keeps in hand. The other leads to a pool thread that runs call() at some later time. The value produced by call() reaches the main program only when the main program asks the Future for it. Read the figure as a relay: the Future is the handle that connects the program's present-time request to the pool's later-time computation.

Checking Progress with isDone

A Future defines several methods. Two of them matter most for retrieving results. The first is isDone(), a boolean-valued method that reports whether the result is available. The second is get(), which retrieves the value the Future holds.

isDone concerns one transition: before the computation completes, the Future reports that its result is not yet available, and after completion it reports that the result is available. Calling isDone is a question the program can ask at any time without changing the Future's state. It simply reads the current status and never waits.

java
Notes on the code
  • submit returns a Future before the value exists
  • isDone answers a yes or no question without blocking
  • Both branches can be reached depending on timing

Calling get and Waiting for the Value

The get method behaves differently from isDone. Calling v.get() retrieves the value, and if the value is not yet available, the call blocks until it is. Blocking means the calling thread pauses at that line and resumes only when the result arrives. Because of this, get is the natural way to wait for an answer, but calling it early will stall the program.

get() calledcaller reaches the lineCaller waitscalling thread pausedcall() finishespool thread produces valueCaller resumesvalue returnedvalue not yet availablestill waitingvalue arrives
When get() is called on a Future whose task is still running, where does the calling thread sit, and when does it resume?
Worked example

Submitting a Callable and reading its value

A program submits a Callable<Integer> that computes a total, then needs that total before printing it. Trace what happens to the calling thread.

  1. 1
    Submit the task

    executor.submit(c) queues the Callable and returns a Future<Integer> right away. The main program holds the Future, not the total.

  2. 2
    Check progress if useful

    isDone() can report whether the total is ready yet. It answers immediately and does not change the Future.

  3. 3
    Ask for the value

    get() is called. If call() has not finished, the calling thread blocks at this line.

  4. 4
    Resume with the result

    When the pool thread finishes call(), the value reaches the Future, get() returns it, and the calling thread continues to the print statement.

Result

The main program never receives the total from submit itself. It receives a Future, and it obtains the integer only through get, which may pause the calling thread until the value is ready.

java
Walk through it
  1. 1
    The lambda is a Callable<Integer> because it returns an integer value.
  2. 2
    submit queues the task and returns a Future<Integer> before the product is computed.
  3. 3
    get is called inside try, because it can raise exceptions.
  4. 4
    If the result is not ready, the line blocks until the pool thread finishes call().

Mistakes with Futures and Tasks

  • Common mistake

    Expecting execute to return a value

    What it looks like
    int total = executor.execute(c);
    Why it fails

    execute accepts a Runnable and returns as soon as the task is queued. A Runnable hands back no value, so there is nothing to assign.

    Do this instead

    Use a Callable<T> and submit it, then read the value through the Future with get.

  • Common mistake

    Assuming submit runs the task in the calling thread

    What it looks like
    executor.submit(c); System.out.println("done");
    Why it fails

    submit does not run the Callable at the moment of submission. The task runs later on a pool thread.

    Do this instead

    Treat the returned Future as the way to learn the outcome, and call get when the value is needed.

  • Common mistake

    Calling get right after submit without intending to wait

    Why it fails

    get blocks until the value is available, so the calling thread pauses at that line.

    Do this instead

    Check isDone first, or call get at the point where the answer is genuinely required.

  • Common mistake

    Leaving get outside a try..catch block

    Why it fails

    get can raise exceptions, so an unhandled call can fail the program.

    Do this instead

    Wrap the call in try..catch and handle the exceptions it can raise.

Practice: Trace the Calls

MEDIUM

A program calls executor.execute(r) with a Runnable r, then immediately calls executor.submit(c) with a Callable<String> c, then calls get on the returned Future. Trace where each step happens: which calls return immediately, which thread runs r and c, and at which point the main thread may be paused.

Hints
  • Ask what execute returns once the task is queued.
  • Ask what submit returns and whether the Callable has finished.
  • Ask when get blocks the calling thread.

Key Takeaways

  • A Runnable is handed to an ExecutorService with execute, which returns as soon as the task is placed in a queue, so the caller does not wait for the task to finish.
  • A Callable<T> declares call, takes no parameters, and returns a value of type T, which is why it can produce a result a Runnable cannot.
  • submit(c) returns a Future<T> that stands in for a value that may not exist yet, and the Callable runs later on a pool thread.
  • isDone() reports whether the result is available without waiting, while get() retrieves the value and blocks until it is available, so it must be wrapped in try..catch.