Concepts / Introduction to Tasks and Threads

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.

  • Programming

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

Definition

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.

Key point

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.

Main threadcalls join()Worker threadbeing joinedstart workerjoin() — main waitsworker terminatesmain resumes
In what order do the main thread and the worker run, and at what point does the main thread wait for the worker to finish?

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.

Main thread reachesjoin()Worker terminated?Main thread stayspausedMain thread resumesCode after join()runsnocheck againyes
Does the main thread continue, block, or exit when it reaches join(), and what decides when it resumes?

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.

python
Notes on the code
  • start() begins each worker so it runs alongside the main program
  • 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 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?

  • Only Worker 1 has to finish

  • Both workers have to terminate

  • The final message prints immediately after start() is called

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

Creates a unit ofworkPauses the callingthreadRuns alongside theprogramWaits until theworker terminatesNo completion signalDoes not stop theworker
How does join() differ from simply starting a thread, and what does the caller do versus what the worker does while join() is pending?
  • Common mistake

    Believing that calling start() is enough to make the program wait for the thread

    Why it fails

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

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

    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.

    Do this instead

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

    Calling join() pauses the calling thread. It does not stop or cancel the joined thread.

    Do this instead

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

    Completion is only guaranteed after the join() call, so code placed earlier has no such guarantee.

    Do this instead

    Place 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()

MEDIUM

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.

Related concepts

More concepts starting with I