Concurrent Programming with Tasks and Threads
A thread is joined when the caller waits for that thread to terminate.
Why a Program Can Finish Too Early
When a program starts a thread, it creates a unit of work that runs alongside the rest of the program. This is useful, but it creates a question that every concurrent program has to answer: how does the program know when that work is finished? The source material for this section makes a specific point about this. Starting a thread does not, by itself, tell the program when the work is done. 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 source focuses on one tool for this job, the join() method. Its purpose is to make sure a program does not exit until its threads have completed. This article follows that single idea: what joining a thread means, how the waiting happens, and where learners tend to go wrong.
Starting Work Versus Waiting for It
It helps to separate two actions that are easy to confuse. Starting a thread sets a unit of work running. Waiting for a thread is a different action: it means the caller pauses until that thread has terminated. The source is explicit that these are different things. Calling join() is how waiting is expressed in code, and 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.
Keep this picture in mind as you read the trace below. The main thread is the one running the program's top-level statements. The worker thread is the one being joined. Starting the worker and joining the worker happen at different moments, and the code that depends on the worker's result belongs after the join.
What join() Does to the Caller
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 flowchart below shows what happens to the caller when it reaches a join. Read it from the top. The caller reaches join() and does not continue past that point while the worker is still running. Only after the worker terminates does the caller resume, and only then can the program move on to whatever comes after the join.
Notice what the caller does not do at the join. It does not stop or cancel the worker. The worker continues on its own until it terminates, and the caller simply waits for that to happen. This distinction is one of the most important points in the source and is repeated in the mistakes section below.
How join() Keeps the Program Alive
The first important fact is that join() ensures a program does not exit until its threads have completed. Imagine a program whose main thread starts a worker and then reaches the end of its code with no join() call. Nothing in that program promises the worker has finished. The program's end is simply reached. By calling join() before the end, the main thread holds its position until the worker is done, so the work the program started is finished before the program moves on.
The second important fact is about placement. Code that depends on a thread's completion should be placed after that thread's join() call. Anything placed before the join may run while the worker is still working, so it cannot safely rely on the worker's results.
Tracing Two Joined Workers
The following example is an original illustration written for this article, not a listing from the source. The source presents no code, so the structure follows the standard Python threading interface, in which a thread is created, started, and then joined. The program starts two worker threads, joins each of them, and only then prints a final message.
- Each worker runs the same function in its own thread, given a different name
start()begins each worker; it does not wait for the worker to finishjoin()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 runs only after both joins have returned
Worker 1 starting
Worker 2 starting
Worker 1 finished
Worker 2 finished
All workers completedTracing where the final message can appear
Two workers are started and then joined. Where in the sequence of events is the final message printed, and what does that guarantee?
- 1Identify the join points
The main thread calls
join()on Worker 1 and then on Worker 2, in that order. - 2Check the position of the final print
The final print comes after both
join()calls in the code, so the main thread reaches it only after both joins return. - 3Apply the meaning of join()
A join returns only after the joined thread has terminated, so both workers have terminated before the final print runs.
- 4State what is not guaranteed
The relative order of the two worker messages is not settled by this code, because the workers run alongside each other.
The final message is printed only after both workers have terminated. The order of the two worker messages before it is not guaranteed.
Misunderstandings About join()
join() is easy to confuse with the actions around it. The comparison below separates what join() does from what other actions do, using the facts the source establishes.
| Action | What it does | What it does not do |
|---|---|---|
| start() on a thread | Sets the thread's work running alongside the rest of the program | Does not tell the program when that work is finished |
| join() on a thread | Pauses the caller until that thread has terminated | Does not start the thread, perform its work, stop it, or cancel it |
| Code placed after join() | Runs once the joined thread has terminated | Is not guaranteed to be safe to run before the join |
What each action establishes, based on the source's description of join()
Common mistake Assuming join() starts the thread
Why it failsThe source states that
join()is not a way to start the thread. Starting is a separate step.Do this insteadStart the thread first, then call join() on it when the program needs to wait.
Common mistake Assuming join() stops or cancels the worker
What it looks likeCalling join() on a worker and expecting the worker to halt at that point.
Why it failsThe source says calling
join()pauses the calling thread; it does not stop or cancel the joined thread.Do this insteadUnderstand join() as waiting only. The worker keeps running until it terminates on its own.
Common mistake Placing dependent code before the join
Why it failsCode placed before the join is not held back until the worker finishes, so it may run while the worker is still working.
Do this insteadPlace code that depends on the thread's completion after that thread's join() call.
Common mistake Leaving out join() and assuming the program waits anyway
Why it failsStarting a thread does not, by itself, tell the program when the work is finished, and the program has no guarantee its threads have completed when it reaches its own end.
Do this insteadCall join() on each thread whose completion the program must wait for.
Predicting What join() Changes
What do you think happens?
A main thread calls join() on a worker that is still running. Does the worker stop at that point?
Reveal answer
No, the worker keeps running until it terminates on its own
The source says calling join() pauses the calling thread at that point; it does not stop or cancel the joined thread. Only the caller waits.
Using the two-worker example, write down the position in the code where a line that reads the workers' results should go, and explain why that position is correct using the rule about dependent code.
Hints
- Think about which line can only run once both workers have terminated.
- The source says code that depends on a thread's completion should be placed after that thread's
join()call.
What the Source Does and Does Not Establish
This section is titled Concurrent Programming with Tasks and Threads and sits within the Introduction to Concurrency module. Its heading names two concepts, Tasks and Threads. The source pack lists both as important and of medium difficulty, but it gives each only a generic description. It does not define either one. The source material for this section consists of the section title itself, with no body text, definitions, examples, or code accompanying it.
Key Takeaways
- A thread is joined when the caller waits for that thread to terminate, and
join()is the method that expresses this waiting. - Starting a thread does not tell the program when its work is finished;
join()ensures a program does not exit until its threads have completed. - Calling
join()pauses the calling thread at that point. It does not stop, cancel, or start the joined thread. - Code that depends on a thread's completion belongs after that thread's
join()call. - The source establishes the Tasks and Threads concepts only by title; their definitions are not in the available material.