multithreading
concurrency
thread synchronization
programming
parallel processing

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

Creating multiple threads is only half the problem. The other half is waiting for them to finish in a controlled way without leaking work, crashing on program exit, or racing shared state. In most languages, the core pattern is start threads, keep their handles, then join or await all of them.

The Core Pattern: Start, Store, Join

At a low level, the standard workflow is:

  1. create the threads
  2. store their handles
  3. join each thread later

Joining means "block here until this thread has finished."

Python Example with threading.Thread

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

This is the classic low-level pattern: launch first, join later.

C++ Example with std::thread

cpp
1#include <iostream>
2#include <thread>
3#include <vector>
4#include <chrono>
5
6void worker(int id) {
7    std::cout << "worker-" << id << " starting\n";
8    std::this_thread::sleep_for(std::chrono::milliseconds(500));
9    std::cout << "worker-" << id << " finished\n";
10}
11
12int main() {
13    std::vector<std::thread> threads;
14
15    for (int i = 0; i < 3; ++i) {
16        threads.emplace_back(worker, i);
17    }
18
19    for (auto& t : threads) {
20        t.join();
21    }
22
23    std::cout << "All threads are done\n";
24}

In C++, every started std::thread must be joined or detached before destruction. Forgetting that is a serious error.

Higher-Level Alternative: Thread Pools

Manual thread creation is not always the best abstraction. For many workloads, a thread pool or task executor is cleaner because it manages worker threads for you.

In Python, ThreadPoolExecutor is often the better default:

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

Calling future.result() waits for completion, and leaving the with block shuts down the pool cleanly.

Waiting for Completion Versus Collecting Results

There are two related goals:

  • wait until every thread has finished
  • collect whatever values the tasks produced

Low-level threads typically require a separate shared result structure plus synchronization. Higher-level executors package waiting and result retrieval together through futures.

Use the abstraction that matches the job instead of defaulting to raw threads.

Shared State Still Needs Protection

Waiting for threads to finish does not solve race conditions. If multiple threads update shared state, you still need synchronization such as:

  • 'Lock in Python'
  • 'std::mutex in C++'

Joining only guarantees that the work ended. It does not make unsynchronized writes correct.

Common Pitfalls

The biggest mistake is starting threads and then not storing the thread handles. Without the handles, you cannot join them cleanly.

Another mistake is joining each thread immediately after starting it in the same loop. That serializes the work and defeats concurrency.

People also confuse threads with tasks. Sometimes a thread pool or async abstraction is a better fit than manual thread creation.

Finally, do not assume that waiting for threads to finish also solves data races. Completion and synchronization are different concerns.

Summary

  • The low-level pattern is create threads, store their handles, then join them.
  • In Python, use threading.Thread and join() or a ThreadPoolExecutor.
  • In C++, use std::thread and join every thread before destruction.
  • Join after starting all threads if you want real overlap.
  • Waiting for completion does not replace proper synchronization for shared state.

Course illustration
Course illustration

All Rights Reserved.