Introduction to Tasks and Threads
The join() method ensures that a program does not exit until its threads have completed. Calling join() pauses the calling thread at that point. It does not stop or cancel the joined thread.
Why Starting Work Is Not Enough
When a program starts one or more threads, it creates units of work that run alongside the rest of the program. The program does not stop and wait for them at the moment they start. The work proceeds in parallel with whatever the program does next.
This creates a problem. Starting a thread does not, by itself, tell the program when that work is finished. If the program reaches its own end without any further coordination, it has no guarantee that its threads have completed. Managing threads means controlling this relationship, so that the program does not finish before the work it started is done.
Keep this question in mind as you read: if a program starts a worker and then has nothing else to do, what stops the program from ending while the worker is still running? The rest of this article answers that question with one tool, the join() method.
What It Means to Join a Thread
A thread is joined when the caller waits for that thread to terminate.
The join() method is how this waiting is expressed in code. Calling join() on a thread asks the program to pause at that point until the thread has terminated. The waiting is the whole purpose of the call. join() does not perform the thread's work, and it is not a way to start the thread.
The join() method ensures that a program does not exit until its threads have completed. Calling join() pauses the calling thread at that point. It does not stop or cancel the joined thread.
The phrase caller is the thread that makes the join() call. The thread being waited on is the joined thread, sometimes called the worker. In the usual pattern, the main thread is the caller and a worker thread is the one being joined.
Tracing the Main Thread and Its Worker
The easiest way to see what join() changes is to follow the order of events between two parties: the main thread that calls join(), and the worker thread being joined. Read the diagram from top to bottom, because each step happens after the one above it.
The key point of the diagram is the waiting. The main thread reaches the join() call and waits. It does not continue past that point until the worker has terminated. Only after the worker terminates does the main thread resume, and only then can the program move on to whatever comes after the join.
How join() Decides When to Resume
The flowchart below turns the same idea into a decision. When the main thread reaches join(), the question it is effectively asking is whether the joined worker has terminated yet. While the answer is no, the main thread stays paused. Once the answer is yes, control passes on to the code that follows.
Notice what join() does not do. It does not end the program when it is reached, and it does not let the caller keep running past that point. The caller is blocked until the joined thread terminates. Code that depends on that thread's completion belongs after its join() call, because only there is completion guaranteed.
Joining Two Workers in Practice
The following example is an original illustration written for this article, not a listing from the source material. The source does not present code, so this structure follows the standard Python threading interface, in which a thread is started and then joined. It starts two worker threads and then calls join() on each of them before the main program prints its final message.
start()begins each worker so it runs alongside the main programjoin()on t1 pauses the main thread until Worker 1 has terminatedjoin()on t2 pauses the main thread until Worker 2 has terminated- The final print comes after both joins
Because the final print comes after both join() calls, the completion message is printed only once both workers have terminated. The order in which the two worker messages appear is not guaranteed by this example, since the two threads run alongside each other. What is guaranteed is the position of the final message relative to the joins.
What do you think happens?
In the example above, what must have happened before the final message is printed?
Reveal answer
Both workers have to terminate
The final print comes after both join() calls. Each join() pauses the main thread until its worker has terminated, so the main thread reaches the print only after both workers have finished.
Common Misunderstandings About join()
Learners often mix up starting a thread with joining it, and they often assume join() does more than it does. The comparison below separates the two roles.
Common mistake Believing that calling start() is enough to make the program wait for the thread
Why it failsStarting a thread creates work that runs alongside the program. Starting it does not, by itself, tell the program when that work is finished.
Do this insteadCall join() on the thread at the point where the program needs its work to be complete.
Common mistake Believing that join() performs the thread's work or starts the thread
Why it failsThe waiting is the whole purpose of the call.
join()does not perform the thread's work, and it is not a way to start the thread.Do this insteadUse start() to begin the thread and join() only to wait for it to terminate.
Common mistake Believing that join() stops or cancels the joined thread
Why it failsCalling
join()pauses the calling thread. It does not stop or cancel the joined thread.Do this insteadExpect the joined thread to keep running to completion while the caller waits.
Common mistake Placing code that depends on a worker's result before its join() call
Why it failsCompletion is only guaranteed after the
join()call, so code placed earlier has no such guarantee.Do this insteadPlace code that depends on the thread's completion after that thread's join() call.
What This Section Does and Does Not Establish
This section is titled Introduction to Tasks and Threads, and it introduces two concepts, Tasks and Threads, within the Introduction to Concurrency module. Its heading names both concepts. That is different from explaining them.
The source material available for this section consists of its title and a small number of statements about join(). It does not supply a definition of a task or a definition of a thread. The concepts are listed as Tasks and Threads, each labeled as important and of medium difficulty, with only generic descriptions that they are concepts related to tasks or to threads.
The section sits inside the Introduction to Concurrency module, which belongs to the chapter Best Practices for Numerical Computations. Concurrency is the broader subject, and this section is one of its entry points. The join() method is the one tool the available material explains for managing thread completion.
Checking Your Understanding of join()
A program starts a worker thread, immediately reads a result that the worker is supposed to produce, and only then calls join() on the worker. Explain why this ordering is risky, and say where the read should go instead.
Hints
- Recall that starting a thread does not tell the program when the work is finished.
- Think about where completion is guaranteed relative to the
join()call. - The read should be placed after the
join()call for that thread.
Key Takeaways
- A thread is joined when the caller waits for that thread to terminate.
join()ensures a program does not exit until its threads have completed, by pausing the calling thread; it does not stop, cancel, or start the joined thread.- Code that depends on a thread's completion should be placed after that thread's
join()call. - The section names two concepts, Tasks and Threads, but the available source material only names them and does not define either one, so naming a concept should not be confused with explaining it.