Concepts / Concurrent Programming with Tasks and Threads

Concurrent Programming with Tasks and Threads

A thread is joined when the caller waits for that thread to terminate.

  • Programming

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

Definition

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.

Caller reachesjoin()Worker terminated?Caller pausesCaller continuesafter join()noonce worker terminatesyes
When the caller reaches join(), does it keep running, and under what condition does it proceed past that call?

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.

python
Notes on the code
  • 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 finish
  • join() on t1 pauses the main thread until Worker 1 has terminated
  • join() on t2 pauses the main thread until Worker 2 has terminated
  • The final print runs only after both joins have returned
Output
Worker 1 starting
Worker 2 starting
Worker 1 finished
Worker 2 finished
All workers completed
Main threadWorker thread1. start() creates the worker2. join() called, main pauses3. worker terminates, main resumes4. main reaches code after join
In what order do the main thread and the worker thread run, and at which point does the main thread pause and resume after calling join()?
Worked example

Tracing 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?

  1. 1
    Identify the join points

    The main thread calls join() on Worker 1 and then on Worker 2, in that order.

  2. 2
    Check 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.

  3. 3
    Apply the meaning of join()

    A join returns only after the joined thread has terminated, so both workers have terminated before the final print runs.

  4. 4
    State 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.

Result

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.

ActionWhat it doesWhat it does not do
start() on a threadSets the thread's work running alongside the rest of the programDoes not tell the program when that work is finished
join() on a threadPauses the caller until that thread has terminatedDoes not start the thread, perform its work, stop it, or cancel it
Code placed after join()Runs once the joined thread has terminatedIs 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 fails

    The source states that join() is not a way to start the thread. Starting is a separate step.

    Do this instead

    Start 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 like

    Calling join() on a worker and expecting the worker to halt at that point.

    Why it fails

    The source says calling join() pauses the calling thread; it does not stop or cancel the joined thread.

    Do this instead

    Understand join() as waiting only. The worker keeps running until it terminates on its own.

  • Common mistake

    Placing dependent code before the join

    Why it fails

    Code placed before the join is not held back until the worker finishes, so it may run while the worker is still working.

    Do this instead

    Place 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 fails

    Starting 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 instead

    Call 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?

  • Yes, the worker stops until the main thread resumes

  • No, the worker keeps running until it terminates on its own

Reveal answer
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.

EASY

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.

Related concepts

More concepts starting with C