Starting and Running Threads
A thread is joined when the caller waits for that thread to terminate.
Why Started Threads Need Coordination
When a program starts one or more threads, it creates units of work that run alongside the rest of the program. 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. The tool this article focuses on for that job is the join() method. Its purpose is to make sure a program does not exit until its threads have completed.
What do you think happens?
If the main program starts a worker thread and then reaches its last line, can the worker still be running at that moment?
Reveal answer
Yes, the worker may still be running. Starting a thread does not wait for it to finish, so nothing in the program is guaranteed to hold the main program back until the worker is done.
Starting a thread launches work that runs alongside the rest of the program. Without a join() call or other coordination, the program has no guarantee that the worker has completed when the main program reaches its end.
What Joining a Thread Means
A thread is joined when the caller waits for that thread to terminate.
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. The join() method does not perform the thread's work itself, and it is not a way to start the thread. Joining is about waiting for completion, not about creating or running the work.
The word joined describes a relationship between two threads. The caller is the thread that executes the join() call, usually the main thread. The thread being joined is the worker. When the caller joins the worker, the caller is the one that waits, and the worker is the one whose termination is awaited.
Tracing the Order of Events
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 sequence below from top to bottom, since each step happens after the one above it.
The key point of the sequence is that 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.
Where the Main Thread Pauses
While the main thread is waiting at join(), it is not running the statements after that call. It is held at the join() point. The worker keeps going, since join() does not stop it. The main thread moves back to running only when the event it is waiting for occurs, which is the worker's termination.
A Worked Example with Two Workers
The following example is an original illustration written for this article, not a listing from the source. It starts two worker threads and then calls join() on each of them before the main program prints its final message. The source does not present code, so the structure follows the standard Python threading interface, in which a thread is started and then joined.
import threading
import time
def worker(name):
print(name + ' starting')
time.sleep(1)
print(name + ' finished')
t1 = threading.Thread(target=worker, args=('Worker 1',))
t2 = threading.Thread(target=worker, args=('Worker 2',))
t1.start()
t2.start()
t1.join()
t2.join()
print('All workers have completed')- 1The worker function prints a starting message, waits one second to simulate work, and then prints a finished message.
- 2Two Thread objects are created, each pointing at the same worker function with a different name.
- 3
start()is called on both threads, so the two workers run alongside the main thread. - 4The main thread reaches
t1.join()and pauses there until Worker 1 has terminated. - 5The main thread then reaches
t2.join()and pauses until Worker 2 has terminated. - 6Only after both joins return does the main thread run the final print statement.
- Each Thread object is created with a target function and its arguments
start()begins each worker so that it runs alongside the main programjoin()on each worker pauses the main thread until that worker has terminated- The final print runs only after both
join()calls have returned
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 join() Does Not Do
A common misunderstanding is that join() is what makes a thread run, or what stops it. The source states that calling join() pauses the calling thread at that point and does not stop or cancel the joined thread. The comparison below shows the same program with and without the join() calls.
The added join() step changes only when the main program moves on. It does not change what the worker does. The worker performs the same work whether or not it is joined. What join() adds is a guarantee about the main program's position: it will not reach its exit until the joined threads have completed.
Common mistake Believing join() starts a thread
Why it failsjoin()does not perform the thread's work and is not a way to start the thread. Starting is done separately, before the join.Do this insteadCall start() to begin the thread's work, and call join() later to wait for that work to finish.
Common mistake Believing join() stops or cancels the worker
Why it failsThe source states that calling
join()pauses the calling thread and does not stop or cancel the joined thread.Do this insteadExpect the worker to keep running to completion. The join() call only makes the caller wait.
Common mistake Placing code that depends on the worker before its join()
Why it failsCode placed before
join()may run while the worker is still working, so it cannot rely on the worker's results.Do this insteadPlace code that depends on a thread's completion after that thread's join() call.
Common mistake Assuming starting a thread is enough for the program to wait for it
Why it failsStarting a thread does not tell the program when the work is finished. Without coordination, the program has no guarantee its threads have completed.
Do this insteadUse join() on each thread the program must wait for before the program's last step.
Practice Tracing a Joined Program
Using the two-worker example, suppose the program is changed so that the final print comes immediately after the two start() calls, before any join() calls. Describe what can happen to the final message relative to the workers, and explain why the original version guarantees a different outcome.
Hints
- Ask whether the main thread pauses anywhere in the changed version
- Compare where the final print sits relative to each
join()call in the original - Remember that
join()pauses the caller until the joined thread terminates
What do you think happens?
In the original two-worker program, when the final message prints, is Worker 1 still running?
Reveal answer
No, Worker 1 has already terminated.
The final print comes after t1.join(), which pauses the main thread until Worker 1 has terminated. The main thread cannot reach the final print until that join has returned.
Key Takeaways
- Joining a thread means the caller waits for that thread to terminate.
- Calling
join()pauses the calling thread at that point, and the paused thread resumes only after the joined thread has terminated. join()keeps a program from exiting before its threads have completed, and code that depends on a worker's completion belongs after that worker'sjoin()call.join()does not start a thread and does not stop or cancel it; it only makes the caller wait.