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:
- create the threads
- store their handles
- join each thread later
Joining means "block here until this thread has finished."
Python Example with threading.Thread
This is the classic low-level pattern: launch first, join later.
C++ Example with std::thread
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:
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:
- '
Lockin Python' - '
std::mutexin 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.Threadandjoin()or aThreadPoolExecutor. - In C++, use
std::threadand 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.

