multithreading
concurrency
thread management
parallel processing
programming techniques

Create multiple threads and wait for all of them to complete

Master System Design with Codemia

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

Introduction

The core pattern for creating multiple threads and waiting for them to finish is simple: start each thread, keep a reference to it, and then join each one. The exact API differs by language, but the idea is the same whether you use raw threads, tasks, or a thread pool.

Raw threads in Python

Python's threading.Thread makes the basic pattern easy to see.

python
1import threading
2import time
3
4def worker(name):
5    print(f"{name} started")
6    time.sleep(1)
7    print(f"{name} finished")
8
9threads = []
10
11for i in range(3):
12    t = threading.Thread(target=worker, args=(f"thread-{i}",))
13    threads.append(t)
14    t.start()
15
16for t in threads:
17    t.join()
18
19print("All threads completed")

start() launches the thread. join() blocks until that specific thread finishes. By joining all threads in a second loop, the main thread waits for all work to complete.

Why you keep thread references

If you do not keep the thread objects, you have nothing to join later. That is why the list of threads matters. It gives you a handle for synchronization after startup.

That pattern generalizes to many languages:

  • create thread
  • store reference
  • start it
  • join later

Exceptions and shared state still matter

Waiting for threads to complete does not automatically make the program correct. If threads write shared data, you may still need locks or thread-safe queues.

Example with a lock:

python
1import threading
2
3counter = 0
4lock = threading.Lock()
5
6def worker():
7    global counter
8    for _ in range(1000):
9        with lock:
10            counter += 1

Without the lock, waiting for the threads with join() would not prevent race conditions.

Prefer a thread pool for many short tasks

For application code, raw threads are often less convenient than a thread pool. In Python, ThreadPoolExecutor gives a cleaner way to run several functions and wait for them all.

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

Calling result() waits for each submitted task, and the context manager handles pool shutdown cleanly.

Threads are not always the right concurrency tool

If the work is CPU-bound, Python threads may not speed it up because of the GIL. For I/O-bound work, threads are often fine. For CPU-heavy work, multiprocessing or native parallel libraries may be better.

That does not change the waiting pattern, but it does change whether threads are a good design choice in the first place.

The ordering of joins matters less than completing them all

You do not have to join threads in the exact order they were started. The important thing is that every started thread is eventually joined or otherwise accounted for before the program assumes the shared work is finished.

Common Pitfalls

  • Starting threads but not storing references, so there is nothing to join later.
  • Joining each thread immediately after starting it, which serializes the work instead of running it concurrently.
  • Assuming join() solves race conditions. It only waits for completion.
  • Using raw threads when a thread pool or async approach would be simpler.
  • Expecting Python threads to scale CPU-bound code the way native parallel processes do.

Summary

  • Start each thread, keep a reference, then join all of them after startup.
  • 'join() is the standard way to wait for a thread to complete.'
  • Use locks or other synchronization primitives when threads share mutable data.
  • For many short tasks, a thread pool is often cleaner than managing raw threads yourself.
  • Waiting for threads is only one part of correct concurrent design.

Course illustration
Course illustration

All Rights Reserved.