multithreading
threading
concurrency
parallel computing
thread synchronization

How to know if other threads have finished?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Knowing whether other threads have finished is a synchronization problem, not something you should solve with ad hoc polling flags. The right answer depends on your concurrency model, but the standard tools are join, futures, latches, or other blocking coordination primitives rather than busy waiting.

Core Sections

join is the simplest tool for a fixed set of threads

If you already have explicit thread objects, join() is usually the clearest answer. It blocks until the target thread finishes.

python
1import threading
2import time
3
4
5def work(name, delay):
6    time.sleep(delay)
7    print(f"done: {name}")
8
9threads = [
10    threading.Thread(target=work, args=("A", 1)),
11    threading.Thread(target=work, args=("B", 2)),
12]
13
14for t in threads:
15    t.start()
16
17for t in threads:
18    t.join()
19
20print("all threads finished")

This is the normal answer when you control the set of worker threads directly.

Futures are better when you want task results too

If the work is submitted through an executor, waiting on futures is often better than managing thread objects yourself.

python
1from concurrent.futures import ThreadPoolExecutor, as_completed
2
3
4def compute(x):
5    return x * x
6
7with ThreadPoolExecutor(max_workers=4) as executor:
8    futures = [executor.submit(compute, i) for i in range(5)]
9
10    for future in as_completed(futures):
11        print(future.result())

This solves two problems at once:

  • you know when the task is finished
  • you can retrieve its result or exception cleanly

That is usually better than inventing your own completion flags.

Timeouts matter in real systems

Waiting forever is dangerous in services, daemons, and interactive applications. Use timeouts when an indefinite wait would be operationally risky.

python
1from concurrent.futures import ThreadPoolExecutor, TimeoutError
2
3with ThreadPoolExecutor(max_workers=2) as executor:
4    future = executor.submit(lambda: "ok")
5    try:
6        print(future.result(timeout=3))
7    except TimeoutError:
8        print("task timed out")

A timeout does not tell you the task finished. It tells you that the wait exceeded a budget. That distinction is important when designing shutdown and recovery logic.

Java examples: join, Future, and CountDownLatch

In Java, the same coordination ideas exist under different APIs.

java
1Thread t1 = new Thread(() -> System.out.println("A done"));
2Thread t2 = new Thread(() -> System.out.println("B done"));
3
4t1.start();
5t2.start();
6
7t1.join();
8t2.join();
9System.out.println("all finished");

If you have a fixed number of workers and one coordinator that should continue only after all of them finish, CountDownLatch is also a strong option.

java
1import java.util.concurrent.CountDownLatch;
2
3CountDownLatch latch = new CountDownLatch(2);
4
5new Thread(() -> { try { /* work */ } finally { latch.countDown(); } }).start();
6new Thread(() -> { try { /* work */ } finally { latch.countDown(); } }).start();
7
8latch.await();
9System.out.println("all done");

This is often cleaner than manually tracking several booleans.

Avoid busy waiting

This kind of loop is usually a bad sign:

python
while not done_flag:
    pass

Or even:

python
while not done_flag:
    time.sleep(0.01)

These approaches waste CPU or introduce unreliable timing behavior. More importantly, they often avoid the real issue: there should be a proper synchronization primitive expressing the handoff between threads.

Completion and success are different questions

A thread finishing does not mean it finished successfully. Good waiting logic also considers exceptions or failure paths.

With futures, exceptions are re-raised when you call result(). That is a major reason futures are often better than raw thread state checks.

Common Pitfalls

  • Polling a shared flag instead of using join, futures, or latches wastes CPU and often hides synchronization bugs.
  • Waiting forever with no timeout can make shutdown and failure recovery harder in production systems.
  • Treating completion as success ignores exceptions raised by worker code.
  • Mixing manual thread management with executor-style task management without a clear ownership model makes lifecycle control harder.
  • Using a primitive boolean without proper synchronization in some languages can create visibility issues between threads.

Summary

  • Use join() when you have a fixed set of thread objects and just need to wait for them to finish.
  • Use futures when you also care about results, exceptions, and flexible task coordination.
  • Use latches or similar primitives when one coordinator needs to wait for several workers.
  • Avoid busy-wait loops and ad hoc polling flags.
  • Always separate the question “has it finished?” from the question “did it succeed?”

Course illustration
Course illustration

All Rights Reserved.