Concepts / Starting and Running Threads

Starting and Running Threads

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

  • Programming

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?

  • Yes, the worker may still be running

  • No, the worker must have finished first

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

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

Main threadWorker threadProgram endstart() launches the workjoin() called, main pausesworker terminatesresumes, then reaches its end
In what order do the main thread's steps and the worker's steps happen relative to the join() call, and where does the main thread pause?

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.

Runningbefore join()Waiting at join()paused until worker endsRunningafter worker terminatesreaches join()worker terminates
What state is the main thread in while it waits at join(), and what event moves it back to running?

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.

Code walkthrough
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')
  1. 1
    The worker function prints a starting message, waits one second to simulate work, and then prints a finished message.
  2. 2
    Two Thread objects are created, each pointing at the same worker function with a different name.
  3. 3
    start() is called on both threads, so the two workers run alongside the main thread.
  4. 4
    The main thread reaches t1.join() and pauses there until Worker 1 has terminated.
  5. 5
    The main thread then reaches t2.join() and pauses until Worker 2 has terminated.
  6. 6
    Only 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 program
  • join() 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.

Start workersStart workersPrint final messagejoin() on each workerProgram exitsPrint final messageProgram exitsnextnextnextnextnext
What is the difference between a program with join() and one without, and does join() itself change what the worker thread does?

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 fails

    join() 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 instead

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

    The source states that calling join() pauses the calling thread and does not stop or cancel the joined thread.

    Do this instead

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

    Code placed before join() may run while the worker is still working, so it cannot rely on the worker's results.

    Do this instead

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

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

    Use join() on each thread the program must wait for before the program's last step.

Practice Tracing a Joined Program

MEDIUM

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?

  • Yes, Worker 1 may still be running

  • No, Worker 1 has already terminated

Reveal answer
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's join() call.
  • join() does not start a thread and does not stop or cancel it; it only makes the caller wait.

Related concepts

More concepts starting with S