Skip to main content

Futures, and when to call get()

A future is a handle to a result that does not exist yet. This lesson is about one decision: where you call get().

Getting it wrong costs more parallelism than any other mistake in this course.

What a future is

future = transform(record)
print(type(future)) # a handle, not your result
print(eugo.hpc.get(future)) # the value

Calling a distributed function returns immediately. The work happens on a worker while your code keeps going. get() blocks until the result is ready.

The mistake

# WRONG: runs one task at a time
results = []
for record in records:
results.append(eugo.hpc.get(transform(record)))

Trace it: dispatch one task, wait for it, dispatch the next, wait again. Only one task is ever in flight. Your cluster of 32 workers has 31 idle at all times, and you are paying for all of them.

The fix

# RIGHT: every task in flight before waiting on any
futures = [transform(record) for record in records]
results = eugo.hpc.get(futures)

Build all the futures, then resolve once.

get() inside the loop4 time unitstask 1task 2task 3task 4dashed = worker waiting, doing nothingget() once, on the list1 time unittask 1task 2task 3task 401234time units, shared by both blocks
Same results, same code length, no error either way. The difference is that the top version waits for each task before dispatching the next, so three of your four workers are idle at every moment. Moving one line out of the loop is frequently the difference between no speedup and near-linear speedup.

The difference is moving one call out of a loop. On a 32-worker cluster it is frequently the difference between no speedup and roughly 25x.

The rule

Build every future first. Call get() once, on the list.

Why this is so easy to get wrong

The broken version looks reasonable. It reads like ordinary Python, produces correct results, and nothing errors. It is only slow. In a distributed system, slowness is easy to blame on the cluster, the data, or the network rather than on your own loop.

When a run disappoints, check this first. It accounts for more missing speedup than everything else combined.

Passing futures to tasks

You can pass a future as an argument. Eugo resolves it before the body runs, which lets you express a dependency:

@eugo.hpc.distribute
def stage_one(x):
return x * 2

@eugo.hpc.distribute
def stage_two(value):
return value + 1

chained = stage_two(stage_one(10)) # no get() in between
print(eugo.hpc.get(chained)) # 21

The intermediate result never returns to your session. It moves directly between workers, saving a round trip.

Accidental serialization

The flip side: a genuine dependency chain cannot parallelize.

# Each task needs the previous result. Serial by construction.
running = seed
for x in items:
running = accumulate(running, x)
result = eugo.hpc.get(running)

No cluster size fixes this. The work has to be restructured, often as a tree reduction that combines pairs in parallel rather than folding left.

Partial results

To handle results as they arrive rather than waiting for all:

futures = [transform(r) for r in records]

while futures:
done, futures = eugo.hpc.wait(futures, num_returns=1)
for f in done:
handle(eugo.hpc.get(f))

Useful for streaming output or showing progress. For most work, one get() on the full list is simpler and just as fast.

What to remember

  • A future is a promise, not a value.
  • get() inside a loop serializes your run.
  • Build all futures, then get() once.
  • Chained futures avoid a round trip through your session.

Video: Working with futures without blocking. The same material, with a transcript.