Nearby lessons

106 of 112

Python Multi Threading - Introduction and Multi Tasking

Python Multi Threading

Multi Threading is introduced under the topic Multi Tasking.

Before understanding Multi Threading, first we should understand what Multi Tasking means.

Multi Threading allows different independent parts of the same program to perform their work concurrently.

What is Multi Tasking?

Multi Tasking means executing several tasks simultaneously.

Simple Definition:

Executing several tasks simultaneously is called Multi Tasking.

For example, while working on a computer, we may perform several activities such as:

  • Writing a Python program
  • Listening to music
  • Downloading files

These activities can execute simultaneously.

Types of Multi Tasking

There are two types of Multi Tasking:

  1. Process Based Multi Tasking
  2. Thread Based Multi Tasking
                 Multi Tasking
                      │
          ┌───────────┴───────────┐
          │                       │
          ▼                       ▼
   Process Based             Thread Based
   Multi Tasking             Multi Tasking

Both approaches allow multiple tasks to make progress, but they differ in how those tasks are organized.

1. Process Based Multi Tasking

Process Based Multi Tasking means executing several tasks simultaneously where each task is a separate independent process.

Definition:

Executing several tasks simultaneously where each task is a separate independent process is called Process Based Multi Tasking.

Here, every task runs as an independent process.

Example of Process Based Multi Tasking

Consider the following example.

While typing a Python program in an editor, we can simultaneously:

  • Listen to MP3 songs
  • Download files from the Internet

Therefore, the computer may be performing:

Task 1 → Python Editor

Task 2 → MP3 Player

Task 3 → Internet Download

All these tasks execute simultaneously and independently.

Hence, this is an example of Process Based Multi Tasking.

Process Based Multi Tasking Diagram

                  Operating System
                         │
            ┌────────────┼────────────┐
            │            │            │
            ▼            ▼            ▼
      Python Editor   MP3 Player   Internet Download
            │            │            │
            ▼            ▼            ▼
        Process 1     Process 2     Process 3

All are separate independent processes.

Best Suitable For Process Based Multi Tasking

Process Based Multi Tasking is best suitable at the Operating System level.

An Operating System can execute and manage several independent applications or processes.

Examples include:

  • Python Editor
  • Music Player
  • Browser
  • File Downloader

Each application can run as a separate process.

2. Thread Based Multi Tasking

Thread Based Multi Tasking means executing several tasks simultaneously where each task is a separate independent part of the same program.

Each independent part is called a Thread.

Definition:

Executing several tasks simultaneously where each task is a separate independent part of the same program is called Thread Based Multi Tasking, and each independent part is called a Thread.

Understanding a Thread

A Thread represents an independent path of execution inside a program.

A single program can contain multiple threads.

Program
   │
   ├──── Thread 1
   │
   ├──── Thread 2
   │
   └──── Thread 3

All these threads belong to the same program.

Thread Based Multi Tasking Diagram

                     Program
                        │
             ┌──────────┼──────────┐
             │          │          │
             ▼          ▼          ▼
          Thread 1   Thread 2   Thread 3
             │          │          │
             └──────────┴──────────┘
                        │
                        ▼
               Same Python Program

All threads belong to the same program.

Best Suitable For Thread Based Multi Tasking

Thread Based Multi Tasking is best suitable at the Programmatic level.

It is useful when one program contains several independent jobs that can make progress concurrently.

One Program
     │
     ├── Job 1 → Thread 1
     ├── Job 2 → Thread 2
     └── Job 3 → Thread 3

Process Based vs Thread Based Multi Tasking

Feature Process Based Multi Tasking Thread Based Multi Tasking
Task Separate independent process Separate independent part of the same program
Execution Unit Process Thread
Program Relationship Tasks can belong to different programs Threads belong to the same program
Best Suitable For Operating System level Programmatic level
Example Editor + MP3 Player + Download Multiple independent jobs inside one program

Advantage of Multi Tasking

Whether it is:

  • Process Based Multi Tasking
  • Thread Based Multi Tasking

the main advantage of Multi Tasking is to improve the performance of the system by reducing response time.

Multiple Independent Tasks
          │
          ▼
Execute Concurrently
          │
          ▼
Better Resource Utilization
          │
          ▼
Reduced Response Time

Applications of Multi Threading

The main application areas of Multi Threading are:

  1. To implement Multimedia Graphics
  2. To develop Animations
  3. To develop Video Games
  4. To develop Web and Application Servers
  5. etc.

These types of applications can contain multiple independent jobs that need to make progress concurrently.

Important Note About Multi Threading

Wherever a group of independent jobs is available, it is highly recommended to execute them simultaneously instead of executing them one by one when concurrent execution is appropriate.

For such cases, we can go for Multi Threading.

Independent Jobs
      │
      ├── Job 1
      ├── Job 2
      └── Job 3
      │
      ▼
Execute Using Multiple Threads
      │
      ▼
Concurrent Execution

Python threading Module

Python provides an inbuilt module called:

threading

This module provides support for developing and managing threads.

Hence, developing multithreaded programs is convenient in Python.

To use the module, we can import it:

import threading

What is the Main Thread?

Every Python program by default contains one thread.

This default thread is called:

MainThread

Simple Definition:

The default thread that starts the execution of a Python program is called the Main Thread.

Even if we do not explicitly create any additional thread, the Python program already executes using the Main Thread.

Python Program Starts
        │
        ▼
   MainThread
        │
        ▼
Execute Program Statements

Program - Print the Name of the Current Executing Thread

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output

Program Explanation

Step 1: Import the threading Module

import threading

The threading module provides support for creating and managing threads.


Step 2: Get the Current Executing Thread

threading.current_thread()

current_thread() returns the currently executing Thread object.

In this program, we have not created any child thread.

Therefore, the current executing thread is the default Main Thread.


Step 3: Get the Thread Name

.getName()

Calling getName() on the Thread object returns the name of that thread.

The default thread name is:

MainThread

Step 4: Display the Thread Name

print("Current Executing Thread:",
      threading.current_thread().getName())

Therefore, the output is:

Current Executing Thread: MainThread

Since every Python program starts with one default thread, its name is MainThread.

Understanding current_thread()

The current_thread() function returns the Thread object representing the thread that is currently executing the statement.

Syntax:

threading.current_thread()

In our example:

threading.current_thread()

returns the object representing:

MainThread

Understanding getName()

getName() returns the name assigned to a Thread object.

Syntax:

thread_object.getName()

Example:

threading.current_thread().getName()

Result:

MainThread

Therefore:

current_thread()
      │
      ▼
Current Thread Object
      │
      ▼
getName()
      │
      ▼
"MainThread"

Execution Flow

Start Python Program
        │
        ▼
Python Starts Default Thread
        │
        ▼
    MainThread
        │
        ▼
Import threading Module
        │
        ▼
Call current_thread()
        │
        ▼
Returns Current Thread Object
        │
        ▼
Call getName()
        │
        ▼
Returns "MainThread"
        │
        ▼
Display Output
        │
        ▼
Current Executing Thread:
MainThread

Process and Thread Concept

Process Based Multi Tasking

Operating System
      │
      ├── Process 1
      ├── Process 2
      └── Process 3


Thread Based Multi Tasking

Single Program
      │
      ├── Thread 1
      ├── Thread 2
      └── Thread 3

The major conceptual difference is that process-based tasks are separate processes, whereas thread-based tasks are independent execution paths inside the same program.

Complete Summary

Topic Description
Multi Tasking Executing several tasks simultaneously.
Types of Multi Tasking Process Based and Thread Based.
Process Based Multi Tasking Each task is a separate independent process.
Best Suitable Level for Process Based Operating System level.
Thread Based Multi Tasking Each task is a separate independent part of the same program.
Thread An independent part or execution path of the same program.
Best Suitable Level for Thread Based Programmatic level.
Main Advantage Improves performance by reducing response time.
Multi Threading Applications Multimedia Graphics, Animations, Video Games, Web Servers and Application Servers.
Python Module threading
Default Thread MainThread
current_thread() Returns the currently executing Thread object.
getName() Returns the name of the Thread object.

Important Notes

  1. Executing several tasks simultaneously is called Multi Tasking.
  2. There are two types of Multi Tasking:
    • Process Based Multi Tasking
    • Thread Based Multi Tasking
  3. In Process Based Multi Tasking, each task is a separate independent process.
  4. Process Based Multi Tasking is best suitable at the Operating System level.
  5. Typing a Python program while listening to MP3 songs and downloading files is an example of Process Based Multi Tasking.
  6. In Thread Based Multi Tasking, each task is a separate independent part of the same program.
  7. Each independent execution part is called a Thread.
  8. Thread Based Multi Tasking is best suitable at the Programmatic level.
  9. The main advantage of Multi Tasking is to improve system performance by reducing response time.
  10. Multi Threading is commonly used for Multimedia Graphics, Animations, Video Games, Web Servers and Application Servers.
  11. When a group of independent jobs is available and concurrent execution is appropriate, Multi Threading can be used instead of executing every job one by one.
  12. Python provides the built-in threading module for thread-related programming.
  13. Every Python program starts with one default thread.
  14. The default thread is called MainThread.
  15. current_thread() returns the currently executing Thread object.
  16. getName() returns the name of a thread.

Python Multi Threading

Multi Threading is introduced under the topic Multi Tasking.

Before understanding Multi Threading, first we should understand what Multi Tasking means.

Multi Threading allows different independent parts of the same program to perform their work concurrently.

What is Multi Tasking?

Multi Tasking means executing several tasks simultaneously.

Simple Definition:

Executing several tasks simultaneously is called Multi Tasking.

For example, while working on a computer, we may perform several activities such as:

  • Writing a Python program
  • Listening to music
  • Downloading files

These activities can execute simultaneously.

Types of Multi Tasking

There are two types of Multi Tasking:

  1. Process Based Multi Tasking
  2. Thread Based Multi Tasking
                 Multi Tasking
                      │
          ┌───────────┴───────────┐
          │                       │
          ▼                       ▼
   Process Based             Thread Based
   Multi Tasking             Multi Tasking

Both approaches allow multiple tasks to make progress, but they differ in how those tasks are organized.

1. Process Based Multi Tasking

Process Based Multi Tasking means executing several tasks simultaneously where each task is a separate independent process.

Definition:

Executing several tasks simultaneously where each task is a separate independent process is called Process Based Multi Tasking.

Here, every task runs as an independent process.

Example of Process Based Multi Tasking

Consider the following example.

While typing a Python program in an editor, we can simultaneously:

  • Listen to MP3 songs
  • Download files from the Internet

Therefore, the computer may be performing:

Task 1 → Python Editor

Task 2 → MP3 Player

Task 3 → Internet Download

All these tasks execute simultaneously and independently.

Hence, this is an example of Process Based Multi Tasking.

Process Based Multi Tasking Diagram

                  Operating System
                         │
            ┌────────────┼────────────┐
            │            │            │
            ▼            ▼            ▼
      Python Editor   MP3 Player   Internet Download
            │            │            │
            ▼            ▼            ▼
        Process 1     Process 2     Process 3

All are separate independent processes.

Best Suitable For Process Based Multi Tasking

Process Based Multi Tasking is best suitable at the Operating System level.

An Operating System can execute and manage several independent applications or processes.

Examples include:

  • Python Editor
  • Music Player
  • Browser
  • File Downloader

Each application can run as a separate process.

2. Thread Based Multi Tasking

Thread Based Multi Tasking means executing several tasks simultaneously where each task is a separate independent part of the same program.

Each independent part is called a Thread.

Definition:

Executing several tasks simultaneously where each task is a separate independent part of the same program is called Thread Based Multi Tasking, and each independent part is called a Thread.

Understanding a Thread

A Thread represents an independent path of execution inside a program.

A single program can contain multiple threads.

Program
   │
   ├──── Thread 1
   │
   ├──── Thread 2
   │
   └──── Thread 3

All these threads belong to the same program.

Thread Based Multi Tasking Diagram

                     Program
                        │
             ┌──────────┼──────────┐
             │          │          │
             ▼          ▼          ▼
          Thread 1   Thread 2   Thread 3
             │          │          │
             └──────────┴──────────┘
                        │
                        ▼
               Same Python Program

All threads belong to the same program.

Best Suitable For Thread Based Multi Tasking

Thread Based Multi Tasking is best suitable at the Programmatic level.

It is useful when one program contains several independent jobs that can make progress concurrently.

One Program
     │
     ├── Job 1 → Thread 1
     ├── Job 2 → Thread 2
     └── Job 3 → Thread 3

Process Based vs Thread Based Multi Tasking

Feature Process Based Multi Tasking Thread Based Multi Tasking
Task Separate independent process Separate independent part of the same program
Execution Unit Process Thread
Program Relationship Tasks can belong to different programs Threads belong to the same program
Best Suitable For Operating System level Programmatic level
Example Editor + MP3 Player + Download Multiple independent jobs inside one program

Advantage of Multi Tasking

Whether it is:

  • Process Based Multi Tasking
  • Thread Based Multi Tasking

the main advantage of Multi Tasking is to improve the performance of the system by reducing response time.

Multiple Independent Tasks
          │
          ▼
Execute Concurrently
          │
          ▼
Better Resource Utilization
          │
          ▼
Reduced Response Time

Applications of Multi Threading

The main application areas of Multi Threading are:

  1. To implement Multimedia Graphics
  2. To develop Animations
  3. To develop Video Games
  4. To develop Web and Application Servers
  5. etc.

These types of applications can contain multiple independent jobs that need to make progress concurrently.

Important Note About Multi Threading

Wherever a group of independent jobs is available, it is highly recommended to execute them simultaneously instead of executing them one by one when concurrent execution is appropriate.

For such cases, we can go for Multi Threading.

Independent Jobs
      │
      ├── Job 1
      ├── Job 2
      └── Job 3
      │
      ▼
Execute Using Multiple Threads
      │
      ▼
Concurrent Execution

Python threading Module

Python provides an inbuilt module called:

threading

This module provides support for developing and managing threads.

Hence, developing multithreaded programs is convenient in Python.

To use the module, we can import it:

import threading

What is the Main Thread?

Every Python program by default contains one thread.

This default thread is called:

MainThread

Simple Definition:

The default thread that starts the execution of a Python program is called the Main Thread.

Even if we do not explicitly create any additional thread, the Python program already executes using the Main Thread.

Python Program Starts
        │
        ▼
   MainThread
        │
        ▼
Execute Program Statements

Program - Print the Name of the Current Executing Thread

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output

Program Explanation

Step 1: Import the threading Module

import threading

The threading module provides support for creating and managing threads.


Step 2: Get the Current Executing Thread

threading.current_thread()

current_thread() returns the currently executing Thread object.

In this program, we have not created any child thread.

Therefore, the current executing thread is the default Main Thread.


Step 3: Get the Thread Name

.getName()

Calling getName() on the Thread object returns the name of that thread.

The default thread name is:

MainThread

Step 4: Display the Thread Name

print("Current Executing Thread:",
      threading.current_thread().getName())

Therefore, the output is:

Current Executing Thread: MainThread

Since every Python program starts with one default thread, its name is MainThread.

Understanding current_thread()

The current_thread() function returns the Thread object representing the thread that is currently executing the statement.

Syntax:

threading.current_thread()

In our example:

threading.current_thread()

returns the object representing:

MainThread

Understanding getName()

getName() returns the name assigned to a Thread object.

Syntax:

thread_object.getName()

Example:

threading.current_thread().getName()

Result:

MainThread

Therefore:

current_thread()
      │
      ▼
Current Thread Object
      │
      ▼
getName()
      │
      ▼
"MainThread"

Execution Flow

Start Python Program
        │
        ▼
Python Starts Default Thread
        │
        ▼
    MainThread
        │
        ▼
Import threading Module
        │
        ▼
Call current_thread()
        │
        ▼
Returns Current Thread Object
        │
        ▼
Call getName()
        │
        ▼
Returns "MainThread"
        │
        ▼
Display Output
        │
        ▼
Current Executing Thread:
MainThread

Process and Thread Concept

Process Based Multi Tasking

Operating System
      │
      ├── Process 1
      ├── Process 2
      └── Process 3


Thread Based Multi Tasking

Single Program
      │
      ├── Thread 1
      ├── Thread 2
      └── Thread 3

The major conceptual difference is that process-based tasks are separate processes, whereas thread-based tasks are independent execution paths inside the same program.

Complete Summary

Topic Description
Multi Tasking Executing several tasks simultaneously.
Types of Multi Tasking Process Based and Thread Based.
Process Based Multi Tasking Each task is a separate independent process.
Best Suitable Level for Process Based Operating System level.
Thread Based Multi Tasking Each task is a separate independent part of the same program.
Thread An independent part or execution path of the same program.
Best Suitable Level for Thread Based Programmatic level.
Main Advantage Improves performance by reducing response time.
Multi Threading Applications Multimedia Graphics, Animations, Video Games, Web Servers and Application Servers.
Python Module threading
Default Thread MainThread
current_thread() Returns the currently executing Thread object.
getName() Returns the name of the Thread object.

Important Notes

  1. Executing several tasks simultaneously is called Multi Tasking.
  2. There are two types of Multi Tasking:
    • Process Based Multi Tasking
    • Thread Based Multi Tasking
  3. In Process Based Multi Tasking, each task is a separate independent process.
  4. Process Based Multi Tasking is best suitable at the Operating System level.
  5. Typing a Python program while listening to MP3 songs and downloading files is an example of Process Based Multi Tasking.
  6. In Thread Based Multi Tasking, each task is a separate independent part of the same program.
  7. Each independent execution part is called a Thread.
  8. Thread Based Multi Tasking is best suitable at the Programmatic level.
  9. The main advantage of Multi Tasking is to improve system performance by reducing response time.
  10. Multi Threading is commonly used for Multimedia Graphics, Animations, Video Games, Web Servers and Application Servers.
  11. When a group of independent jobs is available and concurrent execution is appropriate, Multi Threading can be used instead of executing every job one by one.
  12. Python provides the built-in threading module for thread-related programming.
  13. Every Python program starts with one default thread.
  14. The default thread is called MainThread.
  15. current_thread() returns the currently executing Thread object.
  16. getName() returns the name of a thread.

3. Creating a Thread without Extending Thread Class

The third way of creating a thread is without extending the Thread class.

In this approach, we create a normal Python class containing the required method.

Then we create an object of that class and pass its method as the target of a Thread object.

Simple Definition:

A thread can be created using a method of a normal class as the target without making that class a child of the Thread class.

General structure:

class Test:

    def display(self):
        # Child Thread job

obj = Test()

t = Thread(target=obj.display)
t.start()

Here, Test is a normal class. It does not inherit from Thread.

Basic Concept

Normal Class
   Test
    │
    ▼
display()
    │
    ▼
Create Object
obj = Test()
    │
    ▼
Pass Method as Target
Thread(target=obj.display)
    │
    ▼
Call start()
    │
    ▼
Child Thread Executes
display()

The important point is:

class Test:

and not:

class Test(Thread):

Therefore, this approach does not extend the Thread class.

Program - Creating a Thread without Extending Thread Class

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output

The exact output of this program cannot be predicted.

The Child Thread and Main Thread execute concurrently, so their messages may appear in different orders.

For example, one possible execution could look like:

Important Note About Output

Since multiple threads execute concurrently, the execution order is not fixed.

Therefore, the exact output cannot be predicted.

The output may vary:

  • From one machine to another
  • From one execution to another

Both loops execute ten times, but their output may be interleaved differently.

Possible Run 1:

Child Thread-2
Main Thread-2
Child Thread-2
Main Thread-2
...


Possible Run 2:

Main Thread-2
Main Thread-2
Child Thread-2
Child Thread-2
...

Program Explanation

Step 1: Import Everything from threading

from threading import *

This imports Thread and other threading-related members.


Step 2: Create a Normal Class

class Test:

A normal class named Test is created.

Unlike the previous approach, this class does not extend the Thread class.


Step 3: Create a Member Function

def display(self):

The display() method contains the work that the Child Thread has to perform.


Step 4: Print Child Thread Message

for i in range(10):
    print("Child Thread-2")

The loop executes ten times.

Therefore, the Child Thread prints:

Child Thread-2

ten times.


Step 5: Create an Object

obj = Test()

An object of the Test class is created.

Through this object, we can access:

obj.display

Step 6: Create a Thread Object

t = Thread(target=obj.display)

The display() method of the obj object is passed as the target.

Therefore:

target = obj.display

means that the Child Thread will execute the display() method.


Step 7: Start the Child Thread

t.start()

The Child Thread starts execution.

Its target is:

obj.display

Therefore, the Child Thread executes the display() method.


Step 8: Execute the Main Thread

for i in range(10):
    print("Main Thread-2")

The Main Thread continues executing the remaining program.

Therefore:

  • Child Thread executes obj.display().
  • Main Thread executes its own loop.

Both threads can make progress concurrently.

Understanding Thread(target=obj.display)

The most important statement in this approach is:

t = Thread(target=obj.display)

Here:

Part Meaning
Thread Predefined class from the threading module
t Thread object
target Specifies the callable to be executed by the thread
obj.display Member method that becomes the Child Thread's job
obj
 │
 ▼
display()
 │
 ▼
Passed to target
 │
 ▼
Thread(target=obj.display)
 │
 ▼
t.start()
 │
 ▼
Child Thread Executes display()

Execution Flow - Creating Thread without Extending Thread Class

Create Test Class
       │
       ▼
Create display() Method
       │
       ▼
Create Object
obj = Test()
       │
       ▼
Create Thread Object
Thread(target=obj.display)
       │
       ▼
Call start()
       │
       ▼
Child Thread Starts
       │
       ▼
display() Executes
       │
       ├────────────► Child Thread-2
       │
       ▼
Main Thread Continues
       │
       ▼
Main Thread-2

Three Ways of Creating Threads - Comparison

Method Approach Job Location
Method 1 Without using any class Normal function
Method 2 By extending Thread class Overridden run() method
Method 3 Without extending Thread class Method of a normal class

For Method 3:

Normal Class
    +
Object Method
    +
Thread(target=obj.method)

Without Multi Threading

The document next demonstrates the difference between executing independent functions normally and executing them using multiple threads.

First, consider the program without Multi Threading.

It contains two functions:

  • doubles()
  • squares()

Without threads, these functions execute one after another.

doubles()
    │
    ▼
Completes
    │
    ▼
squares()
    │
    ▼
Completes

Program - Without Multi Threading

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output - Without Multi Threading

The values are printed sequentially because doubles() completes before squares() starts.

A typical output is:

Program Explanation - Without Multi Threading

Function 1: doubles()

def doubles(numbers):
    for n in numbers:
        time.sleep(1)
        print("Double:", 2 * n)

This function calculates and prints the double of every number.

For:

numbers = [1, 2, 3, 4, 5, 6]

the results are:

2
4
6
8
10
12

Before processing each value, the function waits for one second.


Function 2: squares()

def squares(numbers):
    for n in numbers:
        time.sleep(1)
        print("Square:", n * n)

This function calculates and prints the square of every number.

The results are:

1
4
9
16
25
36

This function also waits for one second for every value.


Time Measurement

begintime = time.time()

time.time() returns the current time value.

The starting time is stored in:

begintime

Sequential Execution

doubles(numbers)
squares(numbers)

These two statements execute sequentially.

First:

doubles(numbers)

completes its entire execution.

Only after that:

squares(numbers)

starts execution.

Therefore:

doubles()
   │
   ▼
6 iterations × 1 second
   │
   ▼
Approximately 6 seconds
   │
   ▼
squares()
   │
   ▼
6 iterations × 1 second
   │
   ▼
Approximately 6 seconds

The total is therefore approximately 12 seconds, with small variation due to execution overhead.


Display Total Time

print("The total time taken:",
      time.time() - begintime)

The current time is subtracted from the stored starting time to calculate the elapsed execution time.

Execution Flow - Without Multi Threading

Start Program
      │
      ▼
Create doubles()
      │
      ▼
Create squares()
      │
      ▼
Create Number List
      │
      ▼
Start Timer
      │
      ▼
Execute doubles()
      │
      ▼
Double 1
      │
      ▼
Double 2
      │
      ▼
...
      │
      ▼
Complete doubles()
      │
      ▼
Execute squares()
      │
      ▼
Square 1
      │
      ▼
Square 2
      │
      ▼
...
      │
      ▼
Complete squares()
      │
      ▼
Calculate Total Time
      │
      ▼
Display Total Time

Problem with Sequential Execution

The two functions are independent jobs.

However, without Multi Threading:

doubles()
    │
    ▼
Wait Until Complete
    │
    ▼
squares()

The second function cannot start until the first function finishes.

Because both functions deliberately spend time sleeping, sequential execution takes approximately:

6 seconds + 6 seconds
        =
approximately 12 seconds

This example demonstrates why independent waiting-oriented tasks can benefit from concurrent execution.

With Multi Threading

Now the same two functions are executed using two separate threads.

One thread executes:

doubles(numbers)

and another thread executes:

squares(numbers)

Therefore, both functions can make progress concurrently.

             Start
               │
        ┌──────┴──────┐
        │             │
        ▼             ▼
    Thread-1       Thread-2
        │             │
        ▼             ▼
    doubles()      squares()
        │             │
        └──────┬──────┘
               ▼
        Both Complete

Program - With Multi Threading

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output - With Multi Threading

The exact ordering of the Double and Square lines is not guaranteed because the two child threads execute concurrently.

A possible output is:

Program Explanation - With Multi Threading

Step 1: Create the First Thread

t1 = Thread(
    target=doubles,
    args=(numbers,)
)

The first Child Thread is created.

Its target is:

doubles

Therefore, t1 executes:

doubles(numbers)

Step 2: Create the Second Thread

t2 = Thread(
    target=squares,
    args=(numbers,)
)

The second Child Thread is created.

Its target is:

squares

Therefore, t2 executes:

squares(numbers)

Step 3: Start Both Threads

t1.start()
t2.start()

The first thread starts executing doubles().

The second thread starts executing squares().

Therefore, both functions can make progress concurrently.

t1 ─────► doubles()

t2 ─────► squares()

Step 4: Wait for Both Threads

t1.join()
t2.join()

The Main Thread should not print the total execution time before the Child Threads finish.

Therefore, join() is used.

The Main Thread waits for:

  • t1 to complete
  • t2 to complete

Only after both threads complete does the Main Thread continue.


Step 5: Display Total Execution Time

print("The total time taken:",
      time.time() - begintime)

The total execution time is displayed after both Child Threads finish.

Because both functions spend most of their time sleeping and their waits overlap, the example takes roughly six seconds rather than roughly twelve seconds.

Understanding args=(numbers,)

The thread target function requires one argument:

def doubles(numbers):

Therefore, while creating the thread, we pass the argument using:

args=(numbers,)

Similarly:

t2 = Thread(
    target=squares,
    args=(numbers,)
)

args expects a tuple of positional arguments.

For a single argument, the trailing comma is important:

(numbers,)

This represents a one-element tuple.

Understanding join() in This Program

The statements:

t1.join()
t2.join()

make the Main Thread wait for the Child Threads to finish.

Without these calls, the Main Thread could reach the total-time statement while one or both Child Threads are still executing.

Main Thread
     │
     ▼
Start t1
     │
     ▼
Start t2
     │
     ▼
t1.join()
     │
     ▼
Wait for t1
     │
     ▼
t2.join()
     │
     ▼
Wait for t2 if Needed
     │
     ▼
Print Total Time

Execution Flow - With Multi Threading

Start Timer
      │
      ▼
Create Thread 1
target = doubles
      │
      ▼
Create Thread 2
target = squares
      │
      ▼
Start Thread 1
      │
      ├────────────► doubles()
      │
      ▼
Start Thread 2
      │
      ├────────────► squares()
      │
      ▼
Both Child Threads
Execute Concurrently
      │
      ▼
Main Thread Calls
join()
      │
      ▼
Main Thread Waits
      │
      ▼
Both Threads Complete
      │
      ▼
Calculate Total Time
      │
      ▼
Display Total Time

Without Multi Threading vs With Multi Threading

Feature Without Multi Threading With Multi Threading
Execution Sequential Concurrent
doubles() Executes first Executed by Thread-1
squares() Starts after doubles() completes Executed by Thread-2
Thread Objects Not used t1 and t2
start() Not used Starts both Child Threads
join() Not required Main Thread waits for Child Threads
Approximate Time in This Example 12 seconds 6 seconds

Why Multi Threading Reduces Time in This Example

Each function performs six iterations.

Every iteration contains:

time.sleep(1)

Without Multi Threading:

doubles()
6 × 1 second
      │
      ▼
~6 seconds

+

squares()
6 × 1 second
      │
      ▼
~6 seconds

Total ≈ 12 seconds

With Multi Threading, the waiting periods of both functions overlap:

Thread-1: sleep → print → sleep → print ...
Thread-2: sleep → print → sleep → print ...
              │
              ▼
       Waiting Overlaps
              │
              ▼
       Total ≈ 6 seconds

This timing benefit is especially relevant here because the example spends its time waiting with sleep().

Complete Execution Comparison

WITHOUT MULTI THREADING
-----------------------

Start
  │
  ▼
doubles()
  │
  │ ~6 seconds
  ▼
Complete
  │
  ▼
squares()
  │
  │ ~6 seconds
  ▼
Complete
  │
  ▼
Total ~12 seconds


WITH MULTI THREADING
--------------------

Start
  │
  ├───────────────┐
  ▼               ▼
Thread-1        Thread-2
  │               │
  ▼               ▼
doubles()       squares()
  │               │
  │ ~6 sec        │ ~6 sec
  │               │
  └───────┬───────┘
          ▼
     Both Complete
          │
          ▼
      Total ~6 sec

Summary

Topic Description
Method 3 Create a thread without extending the Thread class.
Normal Class The class does not inherit from Thread.
obj = Test() Creates an object of the normal class.
Thread(target=obj.display) Executes an object's member method in a separate thread.
start() Starts the Child Thread.
Without Multi Threading Functions execute sequentially.
With Multi Threading Independent functions can execute concurrently.
args Passes positional arguments to the target function.
join() Makes the calling thread wait until the specified thread completes.
Sequential Example Approximately 12 seconds.
Threaded Example Approximately 6 seconds because the sleep periods overlap.

Important Notes

  1. A thread can be created using a normal class without extending the Thread class.
  2. In this approach, create an object of the normal class and pass its method as the Thread target.
  3. Thread(target=obj.display) executes the object's display() method in a separate thread after start() is called.
  4. The normal class itself does not need to inherit from Thread.
  5. When the Main Thread and Child Thread execute concurrently, the exact output order cannot be predicted.
  6. In the non-threaded program, doubles() finishes before squares() starts.
  7. Therefore, the non-threaded version executes the two functions sequentially.
  8. In the multithreaded program, one Child Thread executes doubles() and another executes squares().
  9. args=(numbers,) passes the numbers list as an argument to the target function.
  10. The comma in (numbers,) makes it a one-element tuple.
  11. start() starts each Child Thread.
  12. join() makes the Main Thread wait until the Child Thread completes.
  13. The Main Thread waits for both Child Threads before displaying the total execution time.
  14. In this example, sequential execution takes approximately 12 seconds because each function waits about 6 seconds.
  15. With Multi Threading, the waiting periods overlap, so the example takes approximately 6 seconds.
  16. The exact timing can vary slightly depending on the system and execution overhead.

Setting and Getting Name of a Thread

Every thread in Python has a name.

The thread name may be:

  • A default name generated by Python
  • A customized name provided by the programmer

Thread names are useful for identifying different threads while executing and debugging a multithreaded program.

Thread
  │
  ├── Name
  ├── Identification Number
  └── Other Thread Information

Thread Class Methods for Thread Name

We can get and set the name of a thread using the following methods:

Method Description
t.getName() Returns the name of the thread
t.setName(newName) Sets a new name for the thread

Example:

t.getName()

t.setName("MyThread")

Important Note - name Variable

Every Thread has an implicit variable:

name

This variable represents the name of the Thread.

Therefore, we can also access the thread name using:

t.name

For the current executing thread:

current_thread().name

Hence, the thread name can be obtained using:

current_thread().getName()

or:

current_thread().name

Program - Setting and Getting Name of Current Thread

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output

Program Explanation - Thread Name

Step 1: Import Everything from threading

from threading import *

This imports the required threading classes and functions.


Step 2: Display the Current Thread Name

print(current_thread().getName())

current_thread() returns the currently executing Thread object.

At this point, only the default Main Thread is executing.

Therefore:

current_thread().getName()

returns:

MainThread

Step 3: Change the Current Thread Name

current_thread().setName("Pawan Kalyan")

setName() changes the name of the current thread.

Therefore:

MainThread
    │
    ▼
setName("Pawan Kalyan")
    │
    ▼
Pawan Kalyan

Step 4: Display the Updated Thread Name

print(current_thread().getName())

The current thread has already been renamed.

Therefore, the output is:

Pawan Kalyan

Step 5: Access the name Variable

print(current_thread().name)

Every Thread has an implicit variable called name.

It also contains the current thread name.

Therefore, the output is again:

Pawan Kalyan

Execution Flow - Setting and Getting Thread Name

Program Starts
      │
      ▼
Current Thread
      │
      ▼
MainThread
      │
      ▼
getName()
      │
      ▼
Print MainThread
      │
      ▼
setName("Pawan Kalyan")
      │
      ▼
Thread Name Changed
      │
      ▼
getName()
      │
      ▼
Print Pawan Kalyan
      │
      ▼
Access name Property
      │
      ▼
Print Pawan Kalyan

getName(), setName() and name

Member Purpose Example
getName() Gets thread name t.getName()
setName() Changes thread name t.setName("MyThread")
name Represents the thread name t.name

Modern Python Note

The source document uses:

getName()
setName()

These methods are kept here because they are part of the original tutorial example.

In modern Python, directly using the name property is preferred:

# Get thread name
print(current_thread().name)

# Change thread name
current_thread().name = "Pawan Kalyan"

# Get updated name
print(current_thread().name)

This produces the same thread-name concept in a more current style.

Thread Identification Number - ident

For every thread, internally a unique identification number is available.

We can access this identification number using the implicit variable:

ident

Simple Definition:

ident represents the identification number of a Thread.

Syntax:

thread_object.ident

For the current thread:

current_thread().ident

For a Child Thread object:

t.ident

Thread Identification Diagram

Python Program
     │
     ├───────────────┐
     │               │
     ▼               ▼
Main Thread       Child Thread
     │               │
     ▼               ▼
   ident            ident
     │               │
     ▼               ▼
Unique ID          Unique ID

Each thread has its own thread identification number.

Program - Thread Identification Number

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output

Important Note About ident Output

The identification numbers shown in the output are only example values.

2492
2768

The actual identification numbers may be different for each execution or environment.

Therefore, we should not expect:

2492
2768

every time the program runs.

Program Explanation - Thread Identification Number

Step 1: Create the Child Thread Function

def test():
    print("Child Thread")

The test() function contains the work of the Child Thread.


Step 2: Create the Thread Object

t = Thread(target=test)

A Child Thread object is created.

Its target function is:

test

Step 3: Start the Child Thread

t.start()

The Child Thread begins executing the test() function.

It prints:

Child Thread

Step 4: Get Main Thread Identification Number

current_thread().ident

This statement is executed by the Main Thread.

current_thread() returns the Main Thread object and ident gives its identification number.

Example:

Main Thread Identification Number: 2492

Step 5: Get Child Thread Identification Number

t.ident

The variable t refers to the Child Thread object.

Therefore, t.ident returns the identification number of the Child Thread.

Example:

Child Thread Identification Number: 2768

Execution Flow - Thread Identification Number

Program Starts
      │
      ▼
Create test()
      │
      ▼
Create Child Thread
      │
      ▼
t = Thread(target=test)
      │
      ▼
Start Child Thread
      │
      ▼
Child Thread Executes test()
      │
      ▼
Print "Child Thread"
      │
      ▼
Get Main Thread ident
      │
      ▼
current_thread().ident
      │
      ▼
Get Child Thread ident
      │
      ▼
t.ident
      │
      ▼
Display Identification Numbers

active_count() Function

The active_count() function returns the number of Thread objects that are currently active.

Simple Definition:

active_count() returns the number of active threads currently running.

Syntax:

active_count()

For example, if only the Main Thread is active:

active_count()
      │
      ▼
1

If the Main Thread and three Child Threads are active:

MainThread
ChildThread1
ChildThread2
ChildThread3

Total = 4

Program - active_count()

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output - active_count()

Program Explanation - active_count()

Step 1: Import Required Modules

from threading import *
import time

The threading module provides thread-related functionality.

The time module is used to pause execution using sleep().


Step 2: Create display()

def display():
    print(current_thread().getName(), ".started")
    time.sleep(3)
    print(current_thread().getName(), ".ended")

The function:

  1. Prints the current thread name with .started.
  2. Waits for 3 seconds.
  3. Prints the current thread name with .ended.

Step 3: Check Initial Active Thread Count

print("The Number of active Threads:", active_count())

Initially, only the Main Thread is running.

Therefore:

active_count() = 1

Output:

The Number of active Threads: 1

Step 4: Create Three Child Threads

t1 = Thread(target=display, name="ChildThread1")
t2 = Thread(target=display, name="ChildThread2")
t3 = Thread(target=display, name="ChildThread3")

Three Child Thread objects are created.

Each Thread is also given a custom name directly through the name argument.

Thread Object Thread Name Target
t1 ChildThread1 display
t2 ChildThread2 display
t3 ChildThread3 display

Step 5: Start All Three Threads

t1.start()
t2.start()
t3.start()

All three Child Threads begin executing the display() function.

Each thread prints its own name:

ChildThread1 .started
ChildThread2 .started
ChildThread3 .started

Step 6: Count Active Threads Again

print("The Number of active Threads:", active_count())

At this point, the following threads are active:

  1. MainThread
  2. ChildThread1
  3. ChildThread2
  4. ChildThread3

Therefore:

active_count() = 4

Output:

The Number of active Threads: 4

Step 7: Main Thread Waits for 5 Seconds

time.sleep(5)

The Main Thread pauses for five seconds.

Each Child Thread only sleeps for three seconds.

Therefore, during this five-second period, all three Child Threads complete their execution.

They print:

ChildThread1 .ended
ChildThread2 .ended
ChildThread3 .ended

Step 8: Check Active Thread Count Again

print("The Number of active Threads:", active_count())

All three Child Threads have completed.

Only the Main Thread remains active.

Therefore:

active_count() = 1

Output:

The Number of active Threads: 1

How active_count() Changes

Program Starts
      │
      ▼
Only MainThread
      │
      ▼
active_count()
      │
      ▼
      1
      │
      ▼
Create t1, t2, t3
      │
      ▼
Start t1, t2, t3
      │
      ▼
┌─────────────────┐
│ MainThread      │
│ ChildThread1    │
│ ChildThread2    │
│ ChildThread3    │
└─────────────────┘
      │
      ▼
active_count()
      │
      ▼
      4
      │
      ▼
Child Threads Complete
      │
      ▼
Only MainThread
      │
      ▼
active_count()
      │
      ▼
      1

Execution Flow - active_count()

Program Starts
      │
      ▼
Main Thread Running
      │
      ▼
active_count() = 1
      │
      ▼
Create ChildThread1
Create ChildThread2
Create ChildThread3
      │
      ▼
Start All Threads
      │
      ▼
All Child Threads
Execute display()
      │
      ▼
active_count() = 4
      │
      ▼
Main Thread
sleep(5)
      │
      ▼
Child Threads Finish
After About 3 Seconds
      │
      ▼
Only MainThread Remains
      │
      ▼
active_count() = 1

Thread Name, ident and active_count() Comparison

Method / Variable Purpose Example
getName() Returns thread name t.getName()
setName() Changes thread name t.setName("MyThread")
name Represents thread name t.name
ident Returns thread identification number t.ident
active_count() Returns number of currently active threads active_count()

Complete Summary

In this part, we learned how to get important information about Python threads.

Topic Description
Thread Name Every Python Thread has a name.
Default Name Python can automatically provide a thread name.
Custom Name A programmer can assign a custom name.
getName() Returns the thread name.
setName() Changes the thread name.
name Implicit variable/property representing thread name.
ident Identification number associated with a Thread.
active_count() Returns the number of active threads currently running.

Important Notes

  1. Every Python Thread has a name.
  2. The thread name may be the default name generated by Python or a custom name provided by the programmer.
  3. getName() returns the name of a Thread.
  4. setName() changes the name of a Thread.
  5. Every Thread has an implicit variable/property called name.
  6. The name property can also be used to access the thread name.
  7. The original document uses getName() and setName(). Modern Python code generally prefers the name property.
  8. Every Thread has an identification number available through ident.
  9. current_thread().ident returns the identification number of the currently executing Thread.
  10. t.ident returns the identification number associated with the Thread object t.
  11. The exact ident values shown in an example should not be expected on every execution.
  12. active_count() returns the number of currently active threads.
  13. Initially, a normal Python program has the Main Thread, so the example shows active_count() as 1.
  14. When three Child Threads and the Main Thread are active, the example shows an active count of 4.
  15. After all three Child Threads finish, only the Main Thread remains, so the count becomes 1 again.

enumerate() Function

The enumerate() function returns a list of all active threads currently running.

Simple Definition:

enumerate() returns a list containing all currently active Thread objects.

Syntax:

enumerate()

Example:

l = enumerate()

The variable l contains the list of active Thread objects.

We can iterate over this list:

for t in l:
    print("Thread Name:", t.name)

enumerate() Concept

Currently Active Threads

MainThread
ChildThread1
ChildThread2
ChildThread3
       │
       ▼
   enumerate()
       │
       ▼
List of Active
Thread Objects
       │
       ▼
Loop Through List
       │
       ▼
Display Thread Names

Unlike active_count(), which gives only the number of active threads, enumerate() gives the actual Thread objects.

Program - enumerate()

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output - enumerate()

Program Explanation - enumerate()

Step 1: Import Required Modules

from threading import *
import time

The threading module provides thread-related functionality.

The time module is used for sleep().


Step 2: Create display() Function

def display():
    print(current_thread().getName(), ".started")
    time.sleep(3)
    print(current_thread().getName(), ".ended")

The function:

  1. Prints the current thread name with .started.
  2. Waits for 3 seconds.
  3. Prints the thread name with .ended.

Step 3: Create Three Child Threads

t1 = Thread(target=display, name="ChildThread1")
t2 = Thread(target=display, name="ChildThread2")
t3 = Thread(target=display, name="ChildThread3")

Three Child Threads are created.

Object Thread Name
t1 ChildThread1
t2 ChildThread2
t3 ChildThread3

Step 4: Start All Threads

t1.start()
t2.start()
t3.start()

All three Child Threads begin executing.


Step 5: Get All Active Threads

l = enumerate()

enumerate() returns a list containing all currently active threads.

At this point, normally the following four threads are active:

  1. MainThread
  2. ChildThread1
  3. ChildThread2
  4. ChildThread3

Step 6: Display Thread Names

for t in l:
    print("Thread Name:", t.name)

Each Thread object is taken from the list and its name property is displayed.

Therefore:

Thread Name: MainThread
Thread Name: ChildThread1
Thread Name: ChildThread2
Thread Name: ChildThread3

Step 7: Wait for Five Seconds

time.sleep(5)

The Main Thread waits for five seconds.

Each Child Thread waits only three seconds inside display().

Therefore, all Child Threads complete during this period.


Step 8: Call enumerate() Again

l = enumerate()

Now the Child Threads have completed.

Only:

MainThread

remains active.

Therefore, the second loop prints:

Thread Name: MainThread

Execution Flow - enumerate()

Program Starts
      │
      ▼
Create Three Threads
      │
      ▼
Start Threads
      │
      ▼
Child Threads Active
      │
      ▼
enumerate()
      │
      ▼
Returns All Active Threads
      │
      ▼
MainThread
ChildThread1
ChildThread2
ChildThread3
      │
      ▼
Display Thread Names
      │
      ▼
Main Thread Sleeps
for 5 Seconds
      │
      ▼
Child Threads Complete
      │
      ▼
enumerate()
      │
      ▼
Only MainThread
Remains Active

active_count() vs enumerate()

Function Returns Purpose
active_count() Integer Number of currently active threads
enumerate() List of Thread objects Actual currently active threads

Example:

active_count()
      │
      ▼
      4


enumerate()
      │
      ▼
[
 MainThread,
 ChildThread1,
 ChildThread2,
 ChildThread3
]

isAlive() Method

The isAlive() method checks whether a Thread is still executing or not.

Simple Definition:

isAlive() returns True if the Thread is still executing; otherwise, it returns False.

Syntax:

thread_object.isAlive()

Example:

t1.isAlive()

Possible results:

True  → Thread is still executing

False → Thread has completed execution

Program - isAlive()

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output - isAlive()

Program Explanation - isAlive()

Step 1: Create Two Child Threads

t1 = Thread(target=display, name="ChildThread1")
t2 = Thread(target=display, name="ChildThread2")

Two Child Thread objects are created.


Step 2: Start Both Threads

t1.start()
t2.start()

Both Child Threads begin executing display().

Inside display(), each Thread waits for three seconds.


Step 3: Check Whether Threads Are Alive

t1.isAlive()
t2.isAlive()

At this point, both Threads are still executing.

Therefore:

ChildThread1 is Alive : True
ChildThread2 is Alive : True

Step 4: Main Thread Waits Five Seconds

time.sleep(5)

The Main Thread waits for five seconds.

Both Child Threads need only about three seconds to complete.

Therefore, during these five seconds, both Child Threads finish.


Step 5: Check Again

t1.isAlive()
t2.isAlive()

Now both Threads have completed execution.

Therefore:

ChildThread1 is Alive : False
ChildThread2 is Alive : False

Execution Flow - isAlive()

Create Two Threads
      │
      ▼
Start Threads
      │
      ▼
Threads Are Executing
      │
      ▼
isAlive()
      │
      ▼
True
      │
      ▼
Main Thread
Waits 5 Seconds
      │
      ▼
Child Threads Complete
      │
      ▼
isAlive()
      │
      ▼
False

Modern Python Note - is_alive()

The source document uses:

isAlive()

This is preserved in the original example.

In modern Python, the method is:

is_alive()

Therefore, modern code should use:

print(t1.name, "is Alive :", t1.is_alive())
print(t2.name, "is Alive :", t2.is_alive())

The concept remains the same:

True  → Thread is alive

False → Thread is no longer alive

join() Method

If one Thread wants to wait until another Thread completes, we can use the join() method.

Simple Definition:

join() makes the calling Thread wait until the specified Thread completes its execution.

Syntax:

thread_object.join()

Example:

t.join()

If this statement is executed by the Main Thread:

Main Thread
     │
     ▼
t.join()
     │
     ▼
Wait for Thread t
     │
     ▼
Thread t Completes
     │
     ▼
Main Thread Continues

Why Do We Need join()?

Normally, after starting a Child Thread:

t.start()

the Main Thread continues executing independently.

Sometimes we want the Main Thread to continue only after the Child Thread finishes.

For that requirement, we use:

t.join()

Without join():

Main Thread ───────────────►

Child Thread ──────────────►

With join():

Main Thread
    │
    ▼
Start Child
    │
    ▼
join()
    │
    │ WAIT
    │
    ▼
Child Completes
    │
    ▼
Main Thread Continues

Program - join()

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output - join()

Program Explanation - join()

Step 1: Create display()

def display():
    for i in range(10):
        print("Seetha Thread")
        time.sleep(2)

The display() function is the job of the Child Thread.

It prints:

Seetha Thread

ten times.

After every print, it waits for two seconds.


Step 2: Create the Child Thread

t = Thread(target=display)

A Child Thread is created with display as its target.


Step 3: Start the Child Thread

t.start()

The Child Thread starts executing:

display()

Step 4: Main Thread Calls join()

t.join()

This statement is executed by the Main Thread.

Therefore, the Main Thread waits until Thread t completes.

Main Thread
    │
    ▼
t.join()
    │
    ▼
WAIT
    │
    ▼
Child Thread Executes
All 10 Iterations

Step 5: Child Thread Completes

The Child Thread prints:

Seetha Thread

ten times.

Only after the Child Thread finishes does join() return.


Step 6: Main Thread Continues

for i in range(10):
    print("Rama Thread")

Now the Main Thread resumes execution.

It prints:

Rama Thread

ten times.

Therefore, all Seetha Thread lines appear before the Rama Thread lines in this example.

Execution Flow - join()

Main Thread Starts
      │
      ▼
Create Child Thread
      │
      ▼
t.start()
      │
      ▼
Child Thread Starts
      │
      ├──────────────► display()
      │                    │
      │                    ▼
      │              Seetha Thread
      │
      ▼
Main Thread Calls
t.join()
      │
      ▼
Main Thread Waits
      │
      │
      │    Child Thread Continues
      │           │
      │           ▼
      │    Completes 10 Iterations
      │           │
      ◄───────────┘
      │
      ▼
join() Returns
      │
      ▼
Main Thread Continues
      │
      ▼
Print Rama Thread
10 Times

Without join() vs With join()

Without join() With join()
Main and Child Threads can continue concurrently. Calling Thread waits for the specified Thread.
Output may be interleaved. Following statements execute only after the joined Thread completes.
No explicit waiting for Child Thread. join() provides explicit waiting.

In the document's example:

WITHOUT join()

Seetha Thread
Rama Thread
Seetha Thread
Rama Thread
...

Order may vary.


WITH join()

Seetha Thread
Seetha Thread
...
10 times

Then

Rama Thread
Rama Thread
...
10 times

enumerate(), isAlive() and join() Comparison

Method / Function Purpose Return / Effect
enumerate() Find all active Threads Returns list of active Thread objects
isAlive() Check whether a Thread is executing True or False
join() Wait for another Thread to complete Blocks the calling Thread until completion

Thread Information and Management Methods Covered So Far

Method / Function Purpose
current_thread() Returns the currently executing Thread object
getName() Returns the Thread name
setName(name) Changes the Thread name
name Represents the Thread name
ident Provides the Thread identification number
active_count() Returns the number of active Threads
enumerate() Returns the list of active Threads
isAlive() Checks whether a Thread is executing
start() Starts a Thread
run() Contains the job performed by a Thread
join() Waits until the specified Thread completes

Summary

Topic Description
enumerate() Returns a list of all currently active Threads.
Initial enumerate() Example Returns MainThread and the three active Child Threads.
After Child Threads Finish Only MainThread remains in the list.
isAlive() Checks whether a Thread is still executing.
True The Thread is still alive/executing.
False The Thread has completed.
join() Makes the calling Thread wait for another Thread to complete.
join() Example Main Thread waits until all Seetha Thread output is completed before printing Rama Thread.

Important Notes

  1. enumerate() returns a list of all active Threads currently running.
  2. active_count() returns only the number of active Threads, whereas enumerate() returns the actual Thread objects.
  3. In the enumerate() example, initially MainThread and three Child Threads are active.
  4. After waiting five seconds, all three Child Threads complete and only MainThread remains active.
  5. isAlive() checks whether a Thread is still executing.
  6. isAlive() returns True when the Thread is executing.
  7. It returns False after the Thread has completed.
  8. The source document uses isAlive(); modern Python uses is_alive().
  9. join() is used when one Thread needs to wait until another Thread completes its execution.
  10. In the join() example, the Main Thread executes t.join().
  11. Therefore, the Main Thread waits until the Child Thread completes.
  12. The Child Thread prints Seetha Thread ten times before the Main Thread proceeds.
  13. After the Child Thread completes, the Main Thread prints Rama Thread ten times.
  14. join() is important when later statements depend on another Thread having completed.

join(seconds) Method

We can call the join() method with a time period also.

t.join(seconds)

In this case, the calling Thread waits only for the specified amount of time.

Simple Definition:

join(seconds) makes the calling Thread wait for a maximum of the specified number of seconds.

After the specified time is completed, the waiting Thread continues its execution even if the Child Thread has not yet completed.

Main Thread
     │
     ▼
Start Child Thread
     │
     ▼
t.join(seconds)
     │
     ▼
Wait Maximum
Specified Time
     │
     ▼
Timeout Completed
     │
     ▼
Main Thread Continues

Syntax of join(seconds)

The syntax is:

t.join(seconds)
Part Description
t Thread object
join() Method used to make the calling Thread wait
seconds Maximum time the calling Thread should wait

Example:

t.join(5)

This means that the calling Thread waits for Thread t for a maximum of 5 seconds.

Understanding join(5)

Consider:

t.join(5)

Suppose this statement is executed by the Main Thread.

The Main Thread waits for a maximum of five seconds.

There are two possibilities.

Case 1: Child Thread Completes Before 5 Seconds

Child Thread
     │
     ▼
Completes in 3 Seconds
     │
     ▼
join(5) Returns
     │
     ▼
Main Thread Continues

The Main Thread does not need to wait for the full five seconds if the Child Thread finishes earlier.

Case 2: Child Thread Does Not Complete Within 5 Seconds

Child Thread
     │
     ▼
Still Executing
After 5 Seconds

Main Thread
     │
     ▼
join(5)
     │
     ▼
Wait 5 Seconds
     │
     ▼
Timeout
     │
     ▼
Main Thread Continues

Child Thread
     │
     ▼
Continues Its Remaining Work

This second situation occurs in the document's example.

Program - join(seconds)

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output

Important Note About the Output

The output above follows the example given in the document.

However, this is a multithreaded program.

The exact ordering and timing can vary depending on thread scheduling and the system.

The important concept is:

Child Thread Starts
       │
       ▼
Main Thread Waits
Maximum 5 Seconds
       │
       ▼
5 Seconds Completed
       │
       ▼
Main Thread Resumes
       │
       ▼
Child Thread May Still
Be Running

Therefore, after the timeout, output from Rama Thread and the remaining Seetha Thread executions can occur while both threads are active.

Program Explanation

Step 1: Import Required Modules

from threading import *
import time

The threading module is used to create and manage Threads.

The time module is used for creating a delay with:

time.sleep()

Step 2: Create the Child Thread Function

def display():

The display() function contains the work to be performed by the Child Thread.


Step 3: Print the Message Repeatedly

for i in range(10):
    print("Seetha Thread")

The loop executes ten times.

Therefore, the Child Thread prints:

Seetha Thread

ten times.


Step 4: Pause the Child Thread

time.sleep(2)

After printing each message, the Child Thread waits for two seconds.

Therefore, the complete Child Thread requires approximately:

10 iterations × 2 seconds
          =
Approximately 20 seconds

Step 5: Create the Thread

t = Thread(target=display)

A Child Thread object is created.

The target of the Child Thread is:

display

Therefore, Thread t will execute the display() function.


Step 6: Start the Child Thread

t.start()

The Child Thread starts executing:

display()

It begins printing:

Seetha Thread

Step 7: Main Thread Calls join(5)

t.join(5)

This statement is executed by the Main Thread.

The Main Thread waits for Thread t for a maximum of five seconds.

But the Child Thread requires approximately twenty seconds to complete its full work.

Therefore:

Child Thread Total Work
≈ 20 Seconds

Main Thread Waiting Time
= 5 Seconds

The Main Thread does not wait until the Child Thread completes.

It waits only for the specified timeout.


Step 8: Main Thread Continues

for i in range(10):
    print("Rama Thread")

After waiting for five seconds, the Main Thread resumes execution.

It starts printing:

Rama Thread

ten times.

At this time, the Child Thread may still be executing because it has not completed its approximately twenty-second job.

Therefore, both Threads can continue executing after the five-second waiting period.

Why Does This Output Occur?

The document explains the output using the following points:

  1. The Child Thread requires approximately 20 seconds to finish because it prints ten times and waits two seconds after every print.
  2. The Main Thread waits only 5 seconds because of:
t.join(5)
  1. After five seconds, the Main Thread continues its execution.
  2. The Child Thread has not completed its full execution at this point.
  3. Therefore, after the waiting period, the Main Thread and Child Thread can both be active.
0 Seconds
    │
    ▼
Child Thread Starts
    │
    ▼
Main Thread Calls join(5)
    │
    ▼
Main Thread Waiting
    │
    │
    │ Child Thread Continues
    │
    ▼
Approximately 5 Seconds
    │
    ▼
join(5) Timeout
    │
    ▼
Main Thread Continues
    │
    ├──────────────► Rama Thread
    │
    ▼
Child Thread Still Running
    │
    └──────────────► Seetha Thread

Execution Timeline

The basic timing can be understood like this:

Time        Child Thread              Main Thread
-------------------------------------------------------

0 sec       Seetha Thread             start t
                                       │
                                       ▼
                                      join(5)

2 sec       Seetha Thread             Waiting

4 sec       Seetha Thread             Waiting

~5 sec      Still not finished        Timeout completed
                                       │
                                       ▼
                                      Main continues
                                       │
                                       ▼
                                      Rama Thread

6 sec       Seetha Thread             Main may continue

8 sec       Seetha Thread

...         ...

~20 sec     Child completes

The exact scheduling can vary, but the Main Thread does not wait for the Child Thread's full completion because the timeout is only five seconds.

Execution Flow - join(seconds)

Program Starts
      │
      ▼
Import threading
and time
      │
      ▼
Create display()
      │
      ▼
Create Child Thread
      │
      ▼
t.start()
      │
      ▼
Child Thread Starts
      │
      ▼
Print
"Seetha Thread"
      │
      ▼
sleep(2)
      │
      │
      ├────────────────────────┐
      │                        │
      ▼                        ▼
Child Thread             Main Thread
Continues                Calls join(5)
                              │
                              ▼
                       Wait Maximum
                         5 Seconds
                              │
                              ▼
                         Timeout Ends
                              │
                              ▼
                       Main Thread
                         Continues
                              │
                              ▼
                         Prints
                       "Rama Thread"
                              │
      ┌───────────────────────┘
      │
      ▼
Child Thread May
Still Be Executing
      │
      ▼
Child Thread Continues
Until Completion

join() vs join(seconds)

The main difference is the duration for which the calling Thread waits.

join() join(seconds)
Waits until the Thread completes. Waits only for the specified maximum number of seconds.
Main Thread resumes after Child Thread finishes. Main Thread resumes after the timeout if the Child Thread has not finished.
No timeout is specified. A timeout is specified.
The Child Thread is complete when join() returns normally. The Child Thread may still be running when join(seconds) returns because of timeout.

join() Example

t.start()

t.join()

print("Main Thread Continues")

Flow:

Start Child
    │
    ▼
join()
    │
    ▼
Wait
    │
    ▼
Wait
    │
    ▼
Child Completes
    │
    ▼
Main Continues

Here, there is no timeout.

join(seconds) Example

t.start()

t.join(5)

print("Main Thread Continues")

Flow:

Start Child
    │
    ▼
join(5)
    │
    ▼
Wait Maximum
5 Seconds
    │
    ▼
Timeout
    │
    ▼
Main Continues
    │
    ▼
Child May Still
Be Executing

Important Difference - join(5) Does Not Stop the Child Thread

A very important point is that:

t.join(5)

does not mean:

Stop Thread t after 5 seconds

It means:

Wait for Thread t
for maximum 5 seconds

After the timeout:

  • The Main Thread continues.
  • The Child Thread is not automatically terminated.
  • The Child Thread can continue until its work is completed.
join(5)
   │
   ▼
Waiting Timeout
   │
   ├────────► Main Thread Continues
   │
   └────────► Child Thread May Continue

Summary of Methods and Functions Covered So Far

The following methods and functions have been covered in the Multi Threading chapter up to this point.

Method / Function Purpose
current_thread() Returns the currently executing Thread object
getName() Returns the name of the Thread
setName(name) Changes the Thread name
name Implicit variable/property representing the Thread name
ident Returns the identification number of a Thread
active_count() Returns the number of active Threads
enumerate() Returns a list of all active Threads
isAlive() Checks whether a Thread is currently executing
start() Starts a Thread
run() Contains the job executed by the Thread
join() Waits until the specified Thread completes
join(seconds) Waits only for the specified maximum amount of time

Complete Chapter Summary - Up to Part 6

Multi Tasking

  • Process Based Multi Tasking
  • Thread Based Multi Tasking

Thread Creation Methods

  • Creating a Thread without using any class
  • Creating a Thread by extending the Thread class
  • Creating a Thread without extending the Thread class

Thread Information

  • current_thread()
  • getName()
  • setName()
  • name
  • ident

Thread Management

  • start()
  • run()
  • active_count()
  • enumerate()
  • isAlive()

Waiting Methods

  • join()
  • join(seconds)

Summary

Topic Description
join() Waits until another Thread completes.
join(seconds) Waits for a maximum specified amount of time.
t.join(5) Waits for Thread t for at most five seconds.
Timeout After the timeout, the calling Thread continues if the target Thread is still running.
Child Thread May continue executing after the calling Thread resumes.
Example Child Execution Approximately 20 seconds because of ten two-second sleeps.
Main Thread Waiting Only five seconds because join(5) is used.

Important Notes

  1. join(seconds) makes the calling Thread wait only for the specified maximum time.
  2. The syntax is t.join(seconds).
  3. Here, t is the Thread object.
  4. The seconds value specifies the maximum amount of time the calling Thread should wait.
  5. After the specified timeout, the waiting Thread continues execution automatically if the target Thread is still running.
  6. If the target Thread completes before the timeout, join(seconds) returns earlier.
  7. join(seconds) does not terminate the Child Thread.
  8. The Child Thread may continue executing after the Main Thread resumes.
  9. In the document's program, the Child Thread requires approximately twenty seconds because it executes ten iterations with a two-second sleep.
  10. The Main Thread waits only five seconds because t.join(5) is used.
  11. After approximately five seconds, the Main Thread starts printing Rama Thread even though the Child Thread may still be executing.
  12. join() waits until the Thread completes.
  13. join(seconds) waits only for the specified maximum time.
  14. After a join(seconds) timeout, both the Main Thread and Child Thread may continue executing concurrently.
  15. Because Thread scheduling is controlled by the runtime and operating system, the exact interleaving of output can vary.

join(seconds) Method

We can call the join() method with a time period also.

t.join(seconds)

In this case, the Thread will wait only for the specified amount of time.

Simple Definition:

join(seconds) makes the calling Thread wait only for the specified maximum amount of time.

After the specified time is completed, the waiting Thread continues its execution even if the Child Thread has not yet completed.

Syntax

t.join(seconds)

Here:

  • t → Thread object
  • seconds → Maximum time the calling Thread should wait

For example:

t.join(5)

It means the calling Thread waits for Thread t for a maximum of 5 seconds.

After five seconds, the calling Thread can continue even if Thread t is still executing.

How join(seconds) Works

Main Thread
     │
     ▼
Create Child Thread
     │
     ▼
Start Child Thread
     │
     ▼
t.join(5)
     │
     ▼
Main Thread Waits
Maximum 5 Seconds
     │
     ▼
5 Seconds Completed
     │
     ▼
Main Thread Continues
     │
     ├──────────────► Main Thread Work
     │
     ▼
Child Thread May
Still Be Running

The timeout does not terminate the Child Thread.

It only limits how long the calling Thread waits.

Program - join(seconds)

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output

Program Explanation

Step 1: Import Required Modules

from threading import *
import time

The threading module is used to create Threads.

The time module is used for creating delay by using sleep().


Step 2: Create the Child Thread Function

def display():

This function contains the work to be performed by the Child Thread.


Step 3: Print the Message Repeatedly

for i in range(10):
    print("Seetha Thread")

The Child Thread prints:

Seetha Thread

ten times.


Step 4: Pause the Child Thread

time.sleep(2)

After printing each message, the Child Thread waits for two seconds.

Therefore, the Child Thread requires approximately:

10 × 2 seconds
     =
20 seconds

to finish its complete execution.


Step 5: Create the Thread

t = Thread(target=display)

A Child Thread object is created.

The display() function is assigned as the target of the Thread.


Step 6: Start the Child Thread

t.start()

The Child Thread starts executing the display() function.

It starts printing:

Seetha Thread

Step 7: Wait Only for Five Seconds

t.join(5)

This line is executed by the Main Thread.

The Main Thread waits only for 5 seconds.

It does not wait until the Child Thread completes its entire execution.

Compare the timings:

Child Thread Execution Time
≈ 20 seconds

Main Thread Waiting Time
= 5 seconds

Therefore, after approximately five seconds, the Child Thread is normally still executing.


Step 8: Main Thread Continues Execution

for i in range(10):
    print("Rama Thread")

After waiting for five seconds, the Main Thread starts printing:

Rama Thread

even though the Child Thread is still running.

Why Does This Output Occur?

The output occurs because:

  • The Child Thread requires approximately 20 seconds to finish because it prints ten times and waits two seconds after every print.
  • The Main Thread waits only 5 seconds because of join(5).
  • After five seconds, the Main Thread continues its execution.
  • The Child Thread is still executing at that time.
  • Therefore, both Threads execute simultaneously after the waiting period.

Timing Explanation

Child Thread
Total Execution
≈ 20 Seconds

        │
        │
        │
        │
        │
        ▼


Main Thread
join(5)
= 5 Seconds

        │
        ▼
Timeout Completed
        │
        ▼
Main Thread Continues
        │
        ▼
Rama Thread
        │
        │
        └───────────────┐
                        │
Child Thread            │
Still Running ◄─────────┘

The important point is:

join(5)

does not mean:

Terminate the Child Thread
after 5 seconds

It means:

Wait for the Child Thread
for maximum 5 seconds

Execution Flow

Program Starts
      │
      ▼
Create Child Thread
      │
      ▼
Start Child Thread
      │
      ▼
Child Thread Prints
"Seetha Thread"
      │
      ▼
Main Thread Calls
join(5)
      │
      ▼
Main Thread Waits
Only 5 Seconds
      │
      ▼
5 Seconds Complete
      │
      ▼
Main Thread Continues
      │
      ▼
Prints
"Rama Thread"
      │
      ▼
Child Thread Continues
Until Completion

join() vs join(seconds)

join() join(seconds)
Waits until the Thread completes. Waits only for the specified number of seconds.
Main Thread resumes after Child Thread finishes. Main Thread resumes after timeout, even if Child Thread is still running.

Understanding join()

With normal join():

t.start()
t.join()

print("Main Thread")

The Main Thread waits until Thread t completely finishes.

Main Thread
     │
     ▼
t.start()
     │
     ▼
Child Thread Starts
     │
     ▼
t.join()
     │
     ▼
WAIT
     │
     ▼
Child Thread Completes
     │
     ▼
Main Thread Continues

Understanding join(seconds)

With:

t.join(5)

the Main Thread waits for a maximum of five seconds.

Main Thread
     │
     ▼
t.start()
     │
     ▼
Child Thread Starts
     │
     ▼
t.join(5)
     │
     ▼
Wait Maximum
5 Seconds
     │
     ▼
Timeout
     │
     ▼
Main Thread Continues

The Child Thread may still be executing.

Important Concept - Timeout Does Not Stop the Thread

join(seconds) controls the waiting time of the calling Thread.

It does not control the total execution time of the Child Thread.

For example:

t.join(5)

does not stop t after five seconds.

Instead:

Main Thread
     │
     ▼
Waits 5 Seconds
     │
     ▼
Continues


Child Thread
     │
     ▼
Continues Its Work
Until Completion

Summary of All Methods Related to threading Module and Thread

The following methods and functions have been covered in the Multi Threading chapter up to this point.

Method / Function Purpose
current_thread() Returns the currently executing Thread object
getName() Returns the name of the Thread
setName(name) Changes the Thread name
name Implicit variable representing the Thread name
ident Returns the unique identification number of a Thread
active_count() Returns the number of active Threads
enumerate() Returns a list of all active Threads
isAlive() Checks whether a Thread is currently executing
start() Starts a Thread
run() Contains the job executed by the Thread
join() Waits until the specified Thread completes
join(seconds) Waits only for the specified amount of time

Complete Chapter Summary - Up to Part 6

1. Multi Tasking

  • Process Based Multi Tasking
  • Thread Based Multi Tasking

2. Thread Creation Methods

  • Without using any class
  • By extending Thread class
  • Without extending Thread class

3. Thread Information

  • current_thread()
  • getName()
  • setName()
  • name
  • ident

4. Thread Management

  • start()
  • run()
  • active_count()
  • enumerate()
  • isAlive()

5. Waiting Methods

  • join()
  • join(seconds)

Summary

Topic Description
join(seconds) Makes the calling Thread wait only for the specified maximum time.
t.join(5) The calling Thread waits for Thread t for a maximum of five seconds.
After Timeout The waiting Thread continues automatically.
Child Thread May still continue executing after the Main Thread resumes.
join() Waits until the target Thread completely finishes.
join(seconds) Waits only for the given amount of time.

Important Notes

  1. join(seconds) makes the calling Thread wait only for the specified time.
  2. After the specified timeout, the waiting Thread continues execution automatically.
  3. The Child Thread may still continue executing after the Main Thread resumes.
  4. join() waits until the Thread completes, whereas join(seconds) waits only for the given number of seconds.
  5. In the example, the Child Thread requires approximately twenty seconds to complete.
  6. The Main Thread waits only five seconds because join(5) is used.
  7. After five seconds, the Main Thread starts executing its remaining statements.
  8. The Child Thread is not terminated when the timeout completes.
  9. The Child Thread continues until its own execution finishes.
  10. This section completes the summary of the important methods of the threading module and the Thread class covered so far.

Daemon Threads

The Threads which are running in the background are called Daemon Threads.

Simple Definition:

A Daemon Thread is a Thread that runs in the background and provides support to Non-Daemon Threads.

Daemon Threads normally perform supporting activities in the background.

Python Program
      │
      ├────────► Non-Daemon Threads
      │              │
      │              ▼
      │          Main Work
      │
      └────────► Daemon Threads
                     │
                     ▼
               Supporting Work

Main Objective of Daemon Threads

The main objective of Daemon Threads is to provide support for Non-Daemon Threads, such as the Main Thread.

Non-Daemon Thread
       │
       │ Needs Support
       ▼
Daemon Thread
       │
       ▼
Performs Background
Supporting Activity
       │
       ▼
Non-Daemon Thread
Continues Its Work

Therefore:

  • Non-Daemon Threads perform the main work.
  • Daemon Threads provide supporting services in the background.

Example - Garbage Collector

The document gives the Garbage Collector as an example of a Daemon Thread.

Consider the following situation:

  • Whenever the Main Thread runs with low memory,
  • Immediately PVM (Python Virtual Machine) runs the Garbage Collector.
  • Garbage Collector destroys useless objects.
  • It provides free memory.
  • Therefore, the Main Thread can continue its execution without memory problems.

Daemon Thread Illustration

Python Program
      │
      ▼
Main Thread
      │
      ▼
Low Memory Detected
      │
      ▼
Garbage Collector
(Daemon Thread)
      │
      ▼
Destroy Unused Objects
      │
      ▼
Free Memory Available
      │
      ▼
Main Thread Continues

Daemon vs Non-Daemon Thread - Basic Idea

Non-Daemon Thread Daemon Thread
Performs the main work. Performs supporting work.
Main Thread is Non-Daemon. Runs in the background.
Program normally waits for Non-Daemon Threads. Supports Non-Daemon Threads.
Completes its own required work. May terminate automatically when no Non-Daemon Thread remains.

Checking Whether a Thread is Daemon

We can check whether a Thread is a Daemon Thread by using:

  • t.isDaemon()
  • daemon property

Syntax:

t.isDaemon()

t.daemon

Both are used to check the daemon nature of a Thread.

Possible values are:

True
   │
   ▼
Daemon Thread


False
   │
   ▼
Non-Daemon Thread

Program - Checking Daemon Nature of Main Thread

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output

Program Explanation - Checking Daemon Nature

Step 1: Import threading Module

from threading import *

Everything required from the threading module is imported.


Step 2: Check Daemon Nature Using isDaemon()

current_thread().isDaemon()

current_thread() returns the currently executing Thread.

In this program, the currently executing Thread is:

MainThread

isDaemon() checks whether this Thread is a Daemon Thread.

Since the Main Thread is Non-Daemon, the result is:

False

Step 3: Check Using daemon Property

current_thread().daemon

The daemon property also represents whether a Thread is Daemon or Non-Daemon.

Since the current Thread is MainThread:

False

is returned again.

Execution Flow - Checking Daemon Nature

Program Starts
      │
      ▼
Import threading
      │
      ▼
Get Current Thread
      │
      ▼
MainThread
      │
      ▼
isDaemon()
      │
      ▼
False
      │
      ▼
Check daemon Property
      │
      ▼
False

Changing Daemon Nature

We can change the daemon nature of a Thread by using the setDaemon() method.

Syntax:

t.setDaemon(True)

This changes Thread t into a Daemon Thread.

Similarly:

t.setDaemon(False)

represents Non-Daemon nature.

Statement Meaning
t.setDaemon(True) Make the Thread Daemon
t.setDaemon(False) Make the Thread Non-Daemon

Important Rule for setDaemon()

Important Rule:

We can use setDaemon() only before starting the Thread.

Once the Thread starts, we cannot change its daemon nature.

Create Thread
     │
     ▼
setDaemon(True)
     │
     ▼
Allowed
     │
     ▼
start()

But:

Create Thread
     │
     ▼
start()
     │
     ▼
setDaemon(True)
     │
     ▼
Not Allowed
     │
     ▼
RuntimeError

Otherwise, we get:

RuntimeError: cannot set daemon status of active thread

Program - Trying to Change Main Thread Daemon Nature

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output - RuntimeError

Why Does the RuntimeError Occur?

The RuntimeError occurs because:

  • The Main Thread is already started when the Python program begins execution.
  • setDaemon() can be called only before a Thread starts.
  • Therefore, we cannot change the daemon nature of MainThread while it is active.

Hence:

current_thread().setDaemon(True)

results in:

RuntimeError:
cannot set daemon status of active thread

Execution Flow - RuntimeError Example

Python Program Starts
       │
       ▼
MainThread Starts
Automatically
       │
       ▼
MainThread is Already
an Active Thread
       │
       ▼
isDaemon()
       │
       ▼
False
       │
       ▼
setDaemon(True)
       │
       ▼
Trying to Change
Active Thread
       │
       ▼
RuntimeError

Default Nature of Threads

By default, the Main Thread is always Non-Daemon.

For all remaining Threads, daemon nature is inherited from the parent Thread.

  • If the parent Thread is Daemon, the Child Thread is also Daemon.
  • If the parent Thread is Non-Daemon, the Child Thread is also Non-Daemon.
Parent Thread
      │
      ▼
Check Parent
Daemon Nature
      │
      ├───────────────┐
      │               │
      ▼               ▼
   Daemon         Non-Daemon
      │               │
      ▼               ▼
Child Daemon    Child Non-Daemon

Daemon Nature Inheritance

The daemon nature of a newly created Thread is inherited from the Thread that creates it.

For example, normally:

MainThread
Non-Daemon
    │
    ▼
Creates Child Thread
    │
    ▼
Child Thread
Non-Daemon

This happens because the Main Thread is Non-Daemon.

We can explicitly change the Child Thread's daemon nature before starting it:

t = Thread(target=job)

t.setDaemon(True)

t.start()

Program - Changing Child Thread to Daemon

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output

Program Explanation - Changing Child Thread to Daemon

Step 1: Create the job() Function

def job():
    print("Child Thread")

The job() function represents the work of the Child Thread.


Step 2: Create a Normal Child Thread

t = Thread(target=job)

The Main Thread creates Thread t.

Since MainThread is Non-Daemon, the Child Thread also becomes Non-Daemon by default.


Step 3: Check the Initial Daemon Nature

print(t.isDaemon())

The Child Thread inherited its daemon nature from MainThread.

Therefore:

False

Step 4: Change the Daemon Nature

t.setDaemon(True)

The Thread has not been started yet.

Therefore, changing its daemon nature is valid.

Now Thread t becomes a Daemon Thread.


Step 5: Check Again

print(t.isDaemon())

Now the result is:

True

Before and After setDaemon(True)

BEFORE

MainThread
Non-Daemon
     │
     ▼
Creates t
     │
     ▼
Thread t
Non-Daemon

isDaemon()
     │
     ▼
False


AFTER

Thread t
     │
     ▼
setDaemon(True)
     │
     ▼
Thread t
Daemon

isDaemon()
     │
     ▼
True

Execution Flow - Child Thread Daemon Program

Program Starts
      │
      ▼
Main Thread Created
      │
      ▼
Main Thread
(Non-Daemon)
      │
      ▼
Create Child Thread
      │
      ▼
Child Thread
Inherits Parent Nature
      │
      ▼
Non-Daemon
      │
      ▼
isDaemon()
      │
      ▼
False
      │
      ▼
setDaemon(True)
      │
      ▼
Child Thread
Becomes Daemon
      │
      ▼
isDaemon()
      │
      ▼
True

Important Note About Main Thread

The Main Thread is always Non-Daemon.

We cannot change its daemon nature because the Main Thread is already started at the beginning of program execution.

Python Program Starts
       │
       ▼
MainThread Already Running
       │
       ▼
MainThread
Non-Daemon
       │
       ▼
Cannot Change
Daemon Nature
While Active

Modern Python Note

The document uses the older methods:

t.isDaemon()
t.setDaemon(True)

They are preserved in the examples because they are part of the source material.

In modern Python, the preferred daemon property can be used:

# Check daemon status
print(t.daemon)

# Make the thread daemon before start()
t.daemon = True

The fundamental rule remains the same: daemon status must be configured before the Thread is started.

isDaemon(), daemon and setDaemon() Comparison

Method / Property Purpose
t.isDaemon() Checks whether Thread t is Daemon
t.daemon Property representing daemon nature
t.setDaemon(True) Changes Thread to Daemon before it starts
t.setDaemon(False) Changes Thread to Non-Daemon before it starts

Daemon Thread Rules

Rule Description
Main Thread Always Non-Daemon
Child Thread Inherits daemon nature from its parent
Daemon Parent Child is Daemon by default
Non-Daemon Parent Child is Non-Daemon by default
setDaemon() Must be called before start()
Active Thread Daemon nature cannot be changed

Summary

Topic Description
Daemon Thread Runs in the background
Main Objective Support Non-Daemon Threads
Example Garbage Collector
isDaemon() Checks daemon status
daemon Property representing daemon nature
setDaemon(True) Changes daemon nature before Thread starts
Main Thread Always Non-Daemon
Child Thread Inherits daemon nature from parent

Important Notes

  1. Threads running in the background are called Daemon Threads.
  2. The main purpose of Daemon Threads is to support Non-Daemon Threads.
  3. The document gives Garbage Collector as an example of a Daemon Thread.
  4. We can check daemon status using isDaemon() or the daemon property.
  5. False represents a Non-Daemon Thread.
  6. True represents a Daemon Thread.
  7. We can change daemon nature using setDaemon().
  8. setDaemon() can be called only before starting the Thread.
  9. Calling setDaemon() on an active Thread raises RuntimeError.
  10. The Main Thread is always Non-Daemon.
  11. We cannot change the Main Thread's daemon nature because it is already active when the program starts.
  12. Child Threads inherit the daemon nature of their parent Thread.
  13. If the parent is Daemon, the Child Thread is also Daemon by default.
  14. If the parent is Non-Daemon, the Child Thread is also Non-Daemon by default.
  15. A Child Thread created by MainThread is therefore Non-Daemon by default.
  16. We can explicitly convert that Child Thread into a Daemon Thread before calling start().

Termination of Daemon Threads

Daemon Threads are background Threads that provide support to Non-Daemon Threads.

The most important rule regarding the termination of Daemon Threads is:

Whenever the last Non-Daemon Thread terminates, automatically all Daemon Threads will be terminated.

Simple Definition:

A Daemon Thread continues running only while at least one Non-Daemon Thread is alive.

Non-Daemon Thread Running
          │
          ▼
Daemon Thread
Can Continue Running
          │
          ▼
Last Non-Daemon
Thread Terminates
          │
          ▼
All Daemon Threads
Automatically Terminate
          │
          ▼
Program Ends

Main Rule

The complete rule can be understood as:

Situation Daemon Thread Behaviour
At least one Non-Daemon Thread is running Daemon Threads can continue executing
Last Non-Daemon Thread terminates Daemon Threads terminate automatically

This behaviour is demonstrated using the following program.

Demonstration Program

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Understanding the Program

The same program is executed in two different ways.

Case 1

# t.setDaemon(True)

Line-1 is commented.

Therefore, the Child Thread remains Non-Daemon.

Case 2

t.setDaemon(True)

Line-1 is not commented.

Therefore, the Child Thread becomes a Daemon Thread.

Same Program
     │
     ├───────────────┐
     │               │
     ▼               ▼
Line-1          Line-1
Commented       Executed
     │               │
     ▼               ▼
Child =          Child =
Non-Daemon       Daemon

Program Explanation

Step 1: Import Required Modules

from threading import *
import time

The threading module is used to create the Thread.

The time module is used to create a delay using:

time.sleep()

Step 2: Create the Child Thread Function

def job():

The job() function contains the work performed by the Child Thread.


Step 3: Print Lazy Thread Ten Times

for i in range(10):
    print("Lazy Thread")

The Child Thread attempts to print:

Lazy Thread

ten times.


Step 4: Pause After Every Print

time.sleep(2)

After every print, the Child Thread waits for two seconds.

Therefore, its complete execution takes approximately:

10 iterations × 2 seconds
          =
Approximately 20 seconds

Step 5: Create the Thread

t = Thread(target=job)

A Child Thread object is created.

Its target function is:

job

Step 6: Notice Line-1

# t.setDaemon(True)

This line determines whether the Child Thread remains Non-Daemon or becomes Daemon.


Step 7: Start the Child Thread

t.start()

The Child Thread begins executing the job() function.


Step 8: Main Thread Waits

time.sleep(5)

The Main Thread waits for five seconds.

During this period, the Child Thread continues printing:

Lazy Thread

Step 9: Main Thread Prints Final Message

print("End Of Main Thread")

After approximately five seconds, the Main Thread prints:

End Of Main Thread

What happens after this depends on whether the Child Thread is Non-Daemon or Daemon.

Case 1 - Line-1 is Commented

In the first case:

# t.setDaemon(True)

is commented.

Therefore:

  • Main Thread is Non-Daemon.
  • Child Thread is also Non-Daemon.
  • The Child Thread inherits the daemon nature of the Main Thread.
MainThread
Non-Daemon
     │
     ▼
Creates Child Thread
     │
     ▼
setDaemon(True)
NOT Executed
     │
     ▼
Child Thread
Non-Daemon

Since both Threads are Non-Daemon, both execute until their work is completed.

Case 1 - Program

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Case 1 - Output

Case 1 - Output Explanation

Initial Execution

The Child Thread starts executing and prints:

Lazy Thread
Lazy Thread
Lazy Thread

During this time, the Main Thread is waiting because of:

time.sleep(5)

Main Thread Completes Sleep

After five seconds, the Main Thread prints:

End Of Main Thread

Child Thread Continues

The Child Thread does not stop when the Main Thread completes.

It continues printing the remaining messages:

Lazy Thread
Lazy Thread
Lazy Thread
Lazy Thread
Lazy Thread
Lazy Thread
Lazy Thread

until all ten iterations are completed.

Why Does the Child Thread Continue?

The Child Thread continues because:

  • Main Thread is Non-Daemon.
  • Child Thread is also Non-Daemon because setDaemon(True) was not executed.
  • A Non-Daemon Thread continues until its work is completed.
  • Therefore, the program does not terminate simply because the Main Thread has completed.
MainThread
Non-Daemon
     │
     ▼
Finishes
     │
     ▼
Child Thread
Still Non-Daemon
     │
     ▼
Program Must Wait
     │
     ▼
Child Thread
Finishes Remaining Work
     │
     ▼
Program Ends

Case 1 - Execution Flow

Program Starts
      │
      ▼
Main Thread
(Non-Daemon)
      │
      ▼
Create Child Thread
      │
      ▼
Line-1 Commented
      │
      ▼
Child Thread
(Non-Daemon)
      │
      ▼
Child Thread Starts
Printing
"Lazy Thread"
      │
      ▼
Main Thread Waits
5 Seconds
      │
      ▼
Main Thread Prints
"End Of Main Thread"
      │
      ▼
Main Thread Finishes
      │
      ▼
Child Thread is Still
Non-Daemon
      │
      ▼
Child Thread Continues
Running
      │
      ▼
Child Thread Completes
All 10 Iterations
      │
      ▼
Program Ends

Case 1 - Key Observation

When Line-1 is commented, both the Main Thread and Child Thread are Non-Daemon.

Hence:

  • The Main Thread finishes first.
  • The Child Thread is not terminated.
  • The Child Thread continues until all ten iterations are completed.
  • The program ends only after both Non-Daemon Threads finish.

Case 1 - Important Notes

  1. t.setDaemon(True) is commented.
  2. Therefore, the Child Thread remains a Non-Daemon Thread.
  3. The Main Thread waits for five seconds and prints End Of Main Thread.
  4. The Child Thread continues printing Lazy Thread even after the Main Thread finishes.
  5. The program terminates only after both Non-Daemon Threads complete execution.

Case 2 - Line-1 is NOT Commented

Now consider the second case.

Line-1 is executed:

t.setDaemon(True)

Therefore:

  • Main Thread is Non-Daemon.
  • Child Thread becomes Daemon.
MainThread
Non-Daemon
     │
     ▼
Create Child Thread
     │
     ▼
t.setDaemon(True)
     │
     ▼
Child Thread
Daemon

Case 2 - Complete Program

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Case 2 - Output

Case 2 - Program Explanation

Step 1: Create the Child Thread

t = Thread(target=job)

A Child Thread object is created.


Step 2: Convert Child Thread into Daemon

t.setDaemon(True)

The daemon nature is changed before starting the Thread.

Therefore, the Child Thread becomes a Daemon Thread.

Child Thread
Initially Non-Daemon
       │
       ▼
setDaemon(True)
       │
       ▼
Child Thread
Daemon

Step 3: Start the Thread

t.start()

The Daemon Child Thread begins executing job().

It starts printing:

Lazy Thread

Step 4: Main Thread Waits

time.sleep(5)

The Main Thread waits for five seconds.

During these five seconds, the Daemon Thread continues executing.


Step 5: Main Thread Finishes

print("End Of Main Thread")

The Main Thread prints:

End Of Main Thread

After this statement, the Main Thread reaches the end of its work.


Step 6: Daemon Thread Terminates Automatically

The Main Thread is the only Non-Daemon Thread in this program.

When MainThread terminates, no Non-Daemon Thread remains.

Therefore, the Daemon Child Thread is automatically terminated.

Hence, the remaining Lazy Thread messages are not printed.

Why Does the Daemon Thread Stop?

The Daemon Thread stops because:

  • Main Thread is the only Non-Daemon Thread.
  • The Child Thread is a Daemon Thread.
  • Main Thread completes after approximately five seconds.
  • No Non-Daemon Thread remains after MainThread completes.
  • Therefore, Python automatically terminates the Daemon Thread as the interpreter shuts down.
MainThread
Non-Daemon
     │
     ▼
Finishes
     │
     ▼
Any Other
Non-Daemon Thread?
     │
     ▼
     NO
     │
     ▼
Daemon Thread
Does Not Keep
Program Alive
     │
     ▼
Interpreter Shutdown
     │
     ▼
Daemon Thread Ends

Case 2 - Execution Flow

Program Starts
      │
      ▼
Main Thread
(Non-Daemon)
      │
      ▼
Create Child Thread
      │
      ▼
setDaemon(True)
      │
      ▼
Child Thread
(Daemon)
      │
      ▼
Child Thread Starts
Printing
"Lazy Thread"
      │
      ▼
Main Thread Waits
5 Seconds
      │
      ▼
Main Thread Prints
"End Of Main Thread"
      │
      ▼
Main Thread Terminates
      │
      ▼
No Non-Daemon
Thread Exists
      │
      ▼
Interpreter Shuts Down
      │
      ▼
Daemon Thread Ends
      │
      ▼
Program Ends

Why Only Three Lazy Thread Messages?

The Child Thread contains:

for i in range(10):
    print("Lazy Thread")
    time.sleep(2)

It prints once and then waits for two seconds.

Meanwhile, MainThread waits only five seconds:

time.sleep(5)

Therefore, the document's sample execution shows approximately:

Time 0 sec
Lazy Thread

Time 2 sec
Lazy Thread

Time 4 sec
Lazy Thread

Time 5 sec
End Of Main Thread

After MainThread finishes, no Non-Daemon Thread remains.

Therefore, the Daemon Thread does not complete the remaining iterations.

Case 1 vs Case 2

Line-1 Commented Line-1 Not Commented
# t.setDaemon(True) t.setDaemon(True)
Child Thread is Non-Daemon Child Thread is Daemon
Main Thread finishes first Main Thread finishes first
Child Thread continues running Daemon Child does not keep the interpreter alive
Remaining output is printed Remaining output is not printed
Program waits for Child Thread Program can end when MainThread finishes
Child completes all 10 iterations Child may terminate before completing all iterations

Visual Comparison

Non-Daemon Child Thread

MainThread ────────────────► Ends
                               │
                               │
Child Thread ──────────────────┼────────────►
                               │
                               ▼
                         Continues Running
                               │
                               ▼
                         Finishes Work
                               │
                               ▼
                         Program Ends

Daemon Child Thread

MainThread ────────────────► Ends
                               │
                               ▼
                     No Non-Daemon
                     Thread Remains
                               │
                               ▼
                     Interpreter Shutdown

Daemon Thread ─────────────► Ends During Shutdown

Non-Daemon Thread vs Daemon Thread

Non-Daemon Thread Daemon Thread
Continues until its work is completed Primarily used for background/supporting work
Keeps the Python program alive Does not keep the Python program alive by itself
Interpreter waits for its completion Does not prevent interpreter shutdown when no Non-Daemon Threads remain
Main Thread is Non-Daemon Garbage Collector is given as an example in the document
Does not end merely because MainThread ends May end during interpreter shutdown before completing its work

Complete Summary of Daemon Threads

  • Daemon Threads run in the background.
  • Their main objective is to support Non-Daemon Threads.
  • The document gives Garbage Collector as an example of a Daemon Thread.
  • We can check daemon status using:
isDaemon()

daemon
  • We can change daemon nature using:
setDaemon(True)
  • setDaemon() must be called before starting the Thread.
  • Main Thread is always Non-Daemon.
  • Child Threads inherit daemon nature from their parent unless it is changed explicitly.
  • When no Non-Daemon Thread remains, Daemon Threads do not keep the Python program alive.

Important Note - Daemon Thread is Not Gracefully Completed

A Daemon Thread should not be used for work that must definitely finish before the program exits.

For example, in this program:

for i in range(10):
    print("Lazy Thread")
    time.sleep(2)

the Daemon Thread intends to execute ten iterations.

But it may execute only a few iterations before interpreter shutdown begins.

Expected Work
10 Iterations
      │
      ▼
MainThread Ends Early
      │
      ▼
Interpreter Shutdown
      │
      ▼
Daemon Thread
May Not Finish
All 10 Iterations

The important concept is that Daemon Threads do not keep the interpreter alive by themselves.

Modern Python Syntax

The source document uses:

t.setDaemon(True)

The same concept in modern Python is normally written using the daemon property:

t = Thread(target=job)

t.daemon = True

t.start()

We can also specify the daemon nature while creating the Thread:

t = Thread(target=job, daemon=True)

t.start()

The important rule remains the same:

Daemon status must be configured before start() is called.

Complete Execution Concept

                Python Program
                      │
                      ▼
                 MainThread
                Non-Daemon
                      │
                      ▼
               Create Child
                      │
             ┌────────┴────────┐
             │                 │
             ▼                 ▼
       Non-Daemon           Daemon
          Child              Child
             │                 │
             ▼                 ▼
       MainThread Ends    MainThread Ends
             │                 │
             ▼                 ▼
       Child Still       No Non-Daemon
       Keeps Program     Thread Remains
          Alive               │
             │                 ▼
             ▼          Interpreter Can
       Child Finishes       Shut Down
             │                 │
             ▼                 ▼
       Program Ends      Daemon Thread Ends

Summary

Topic Description
Daemon Termination Daemon Threads do not keep the interpreter alive after all Non-Daemon Threads finish.
Case 1 setDaemon(True) is commented.
Case 1 Child Non-Daemon
Case 1 Result Child continues after MainThread finishes.
Case 2 setDaemon(True) is executed.
Case 2 Child Daemon
Case 2 Result Program can shut down when MainThread finishes.
Non-Daemon Thread Keeps the program alive until it completes.
Daemon Thread Does not keep the program alive by itself.

Important Notes

  1. t.setDaemon(True) converts the Child Thread into a Daemon Thread.
  2. The daemon nature must be changed before calling start().
  3. Main Thread always remains a Non-Daemon Thread.
  4. Daemon Threads are used to support Non-Daemon Threads in the background.
  5. When the last Non-Daemon Thread terminates, Daemon Threads do not keep the interpreter alive.
  6. In the document's Daemon example, only three Lazy Thread messages are shown before End Of Main Thread.
  7. The remaining messages are not printed because the program shuts down after MainThread finishes and no Non-Daemon Thread remains.
  8. If the Child Thread is Non-Daemon, it continues execution even after MainThread completes.
  9. If the Child Thread is Daemon, it does not keep the program running after MainThread terminates.
  10. A Non-Daemon Child Thread normally completes all of its required work before program termination.
  11. A Daemon Thread may not complete all of its work before interpreter shutdown.
  12. The document compares both cases using the same program so that the difference between Daemon and Non-Daemon behaviour is clear.

Synchronization

If multiple Threads are executing simultaneously, then there may be a chance of data inconsistency problems.

This problem can occur when multiple Threads access the same shared resource at the same time.

To overcome this problem, we should use Synchronization.

Simple Definition:

Synchronization means allowing Threads to execute one by one so that data inconsistency problems can be avoided.

The document defines it simply as:

Synchronization means
at a time only one Thread.

Why Do We Need Synchronization?

In Multi Threading, multiple Threads can execute simultaneously.

This is useful for improving concurrency, but it can create problems when multiple Threads work with the same shared resource.

For example:

Thread-1
    │
    ▼
Shared Resource
    ▲
    │
Thread-2

If both Threads access the shared resource at the same time, their executions may overlap.

This overlapping execution can create:

  • Data inconsistency problems
  • Irregular output

Problem Without Synchronization

Consider two Threads executing the same function simultaneously.

Thread-1 ───────┐
                │
                ▼
            wish()
                ▲
                │
Thread-2 ───────┘

Both Threads enter the same function at approximately the same time.

The execution of one Thread may overlap with the execution of another Thread.

As a result, we may get mixed or irregular output.

Example Program - Without Synchronization

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output

Important Note About the Output

The exact order of the output can vary from one execution to another because Thread scheduling is not fixed.

The important observation from the example is that the output becomes mixed.

Instead of getting:

Good Evening:Dhoni
Good Evening:Dhoni

or

Good Evening:Yuvraj
Good Evening:Yuvraj

we may get output such as:

Good Evening:Good Evening:Yuvraj
Dhoni

This happens because both Threads are executing the same function simultaneously.

Program Explanation

Step 1: Import Required Modules

from threading import *
import time

The threading module is used for creating Threads.

The time module is used for delaying execution by using:

time.sleep()

Step 2: Create the Shared Function

def wish(name):

The wish() function is the shared function.

The same function will be executed by multiple Threads.


Step 3: Repeat Ten Times

for i in range(10):

Each Thread executes the loop ten times.

Therefore:

Thread t1
   │
   ▼
10 Iterations

Thread t2
   │
   ▼
10 Iterations

Step 4: Print the Greeting

print("Good Evening:", end='')

This statement prints:

Good Evening:

The end='' argument prevents print() from moving to the next line.

Therefore, Python waits for the next output to continue on the same line.


Step 5: Pause the Thread

time.sleep(2)

After printing Good Evening:, the currently executing Thread sleeps for two seconds.

This delay gives another Thread an opportunity to execute.


Step 6: Print the Name

print(name)

After the sleep period, the Thread prints the name passed to the function.

For example:

Dhoni

or

Yuvraj

Step 7: Create Two Threads

t1 = Thread(target=wish, args=("Dhoni",))
t2 = Thread(target=wish, args=("Yuvraj",))

Two Child Threads are created.

Both Threads execute the same wish() function.

Thread Function Argument
t1 wish() "Dhoni"
t2 wish() "Yuvraj"

Step 8: Start Both Threads

t1.start()
t2.start()

After starting both Threads, they execute concurrently.

There is no synchronization mechanism in this program.

Therefore, both Threads can enter wish() and their executions can overlap.

How the Irregular Output is Produced

Consider one possible execution.

Step 1

Thread t1 executes:

print("Good Evening:", end='')

Output:

Good Evening:

Step 2

Thread t1 executes:

time.sleep(2)

Now t1 sleeps.


Step 3

During this period, Thread t2 gets a chance to execute:

print("Good Evening:", end='')

The output can now become:

Good Evening:Good Evening:

Step 4

After the sleeping Threads resume, the names may be printed:

Yuvraj
Dhoni

Therefore, we can get:

Good Evening:Good Evening:Yuvraj
Dhoni

This is an example of irregular output caused by overlapping Thread execution.

Execution Flow Without Synchronization

Program Starts
      │
      ▼
Create Thread t1
      │
      ▼
Create Thread t2
      │
      ▼
Start t1
      │
      ▼
t1 Enters wish()
      │
      ▼
Print "Good Evening:"
      │
      ▼
t1 Sleeps
      │
      ├────────────────────┐
      │                    │
      │                    ▼
      │                t2 Executes
      │                    │
      │                    ▼
      │             Enters wish()
      │                    │
      │                    ▼
      │           Print "Good Evening:"
      │                    │
      └────────────┬───────┘
                   │
                   ▼
            Threads Resume
                   │
                   ▼
             Names Printed
                   │
                   ▼
              Mixed Output

Why Are We Getting This Output?

We are getting irregular output because both Threads are executing the wish() function simultaneously.

The execution of one Thread overlaps with the execution of another Thread.

As a result:

  • Both Threads can print Good Evening: almost at the same time.
  • The names can be printed in an irregular order.
  • The output becomes mixed.
t1 ─────► Good Evening:
              │
              ▼
            sleep()

t2 ─────► Good Evening:
              │
              ▼
            sleep()

Both Resume
     │
     ▼
Names Printed
in Scheduler-Dependent Order

What Is the Problem?

Simultaneous execution can cause:

  • Irregular output
  • Data inconsistency problems

This happens because multiple Threads are accessing the same function or shared resource at the same time.

Multiple Threads
       │
       ▼
Same Shared Resource
       │
       ▼
Simultaneous Access
       │
       ▼
Overlapping Execution
       │
       ▼
Irregular Output /
Data Inconsistency

Data Inconsistency

Data inconsistency means the shared data may not remain in the expected or correct state when multiple Threads modify or access it simultaneously without proper coordination.

Conceptually:

Shared Data
    │
    ├────────► Thread-1
    │
    └────────► Thread-2
         │
         ▼
Both Access
Simultaneously
         │
         ▼
Unexpected Result

The example in this section demonstrates the problem using irregular output.

How Can We Solve This Problem?

To overcome this problem, we should use:

Synchronization

Synchronization controls access to the shared resource.

Instead of allowing multiple Threads to execute the critical work simultaneously, the Threads are coordinated so that only the permitted Thread accesses it at that time.

Without Synchronization

Thread-1 ───┐
            ├──► Shared Resource
Thread-2 ───┘


With Synchronization

Thread-1 ─────► Shared Resource
                    │
                    ▼
                Complete
                    │
                    ▼
Thread-2 ─────► Shared Resource

What Is Synchronization?

In synchronization, Threads are executed one by one for the protected work so that we can overcome data inconsistency problems.

Definition:

Synchronization means at a time only one Thread.

In simple words:

Thread-1
   │
   ▼
Execute Protected Work
   │
   ▼
Complete / Release
   │
   ▼
Thread-2
   │
   ▼
Execute Protected Work

This prevents overlapping access to the synchronized shared resource.

Execution Without Synchronization

Start
  │
  ▼
Thread-1 Starts
  │
  ├────────────────┐
  │                │
  ▼                ▼
Thread-1       Thread-2 Starts
Executing      Simultaneously
  │                │
  └────────┬───────┘
           │
           ▼
    Overlapping Work
           │
           ▼
       Mixed Output
           │
           ▼
    Data Inconsistency

Without synchronization, multiple Threads can access the same shared resource concurrently.

Execution With Synchronization

Start
  │
  ▼
Thread-1 Executes
Protected Work
  │
  ▼
Thread-1 Finishes /
Releases Resource
  │
  ▼
Thread-2 Executes
Protected Work
  │
  ▼
Thread-2 Finishes
  │
  ▼
Correct / Regular
Result

Synchronization prevents the protected operations of the Threads from interfering with one another.

Without Synchronization vs With Synchronization

Without Synchronization With Synchronization
Multiple Threads may access the shared resource simultaneously. Shared access is controlled.
Thread executions may overlap. Protected execution occurs one Thread at a time.
Output may become mixed. Output becomes regular for the protected operation.
Data inconsistency may occur. Data inconsistency can be avoided.
Shared resource can be accessed together. Shared resource is accessed according to synchronization control.

Main Application Areas of Synchronization

The document gives the following common application areas of Synchronization:

  1. Online Reservation System
  2. Funds Transfer from Joint Accounts
  3. etc.

Example - Online Reservation System

Consider an Online Reservation System.

Multiple users may try to reserve the same seat at approximately the same time.

Available Seat
      │
      ├────────► User / Thread-1
      │
      └────────► User / Thread-2

If access to the reservation operation is not controlled properly, conflicting updates can occur.

Synchronization can protect the critical reservation operation so that shared booking data is updated safely.

Example - Funds Transfer from Joint Accounts

Another application mentioned in the document is:

Funds Transfer
from Joint Accounts

If multiple Threads perform operations on the same shared account data simultaneously, incorrect updates may occur.

Synchronization can ensure that the critical balance-update operation is properly coordinated.

Shared Account
     │
     ▼
Thread-1 Requests Access
     │
     ▼
Protected Operation
     │
     ▼
Thread-1 Completes
     │
     ▼
Thread-2 Gets Access

Important Concept - Critical Section

The part of a program that accesses shared data and therefore requires controlled access is commonly called a critical section.

def wish(name):

    # Critical / Protected Work
    print("Good Evening:", end='')
    time.sleep(2)
    print(name)

Synchronization mechanisms are used to control access to such protected code.

In the upcoming section, the document uses:

Lock

to implement synchronization.

Conceptual Execution Flow

Multiple Threads
       │
       ▼
Need Shared Resource
       │
       ▼
Is Synchronization
Used?
       │
   ┌───┴────┐
   │        │
   ▼        ▼
  No       Yes
   │        │
   ▼        ▼
Threads    Controlled
Overlap    Access
   │        │
   ▼        ▼
Mixed      One Thread
Output     at a Time
   │        │
   ▼        ▼
Possible   Regular /
Data       Consistent
Problem    Result

Synchronization Does Not Mean Only One Thread Exists

Synchronization does not mean that the entire Python program can contain only one Thread.

Multiple Threads can still exist.

The important point is that access to the synchronized shared resource is controlled.

Python Program
     │
     ├────► Thread-1
     ├────► Thread-2
     └────► Thread-3

All Threads Exist

But

Critical Shared Resource
        │
        ▼
Controlled Access

This distinction is important when understanding Lock, RLock, and Semaphore.

What Comes After Synchronization?

Synchronization is the general concept.

Python's threading module provides synchronization mechanisms to implement it.

The document continues with:

Synchronization
      │
      ▼
Lock
      │
      ▼
RLock
      │
      ▼
Semaphore

The next topic starts with Synchronization Using Lock.

Summary

Topic Description
Simultaneous Execution Multiple Threads execute concurrently.
Problem Execution may overlap when shared resources are accessed.
Irregular Output Output from multiple Threads can become mixed.
Data Inconsistency Shared data may produce unexpected results.
Solution Synchronization
Synchronization Controls Thread access to shared resources.
Document Definition At a time only one Thread.
Application 1 Online Reservation System
Application 2 Funds Transfer from Joint Accounts

Important Notes

  1. Simultaneous execution of multiple Threads may create data inconsistency problems.
  2. The wish() function demonstrates this problem using irregular output.
  3. Both t1 and t2 execute the same wish() function.
  4. Thread t1 receives "Dhoni" as its argument.
  5. Thread t2 receives "Yuvraj" as its argument.
  6. The irregular output occurs because both Threads execute the same function concurrently and their operations overlap.
  7. time.sleep(2) pauses the currently executing Thread for two seconds.
  8. During the sleep period, another Thread can execute.
  9. Because end='' is used, Good Evening: is printed without immediately moving to the next line.
  10. Without synchronization, the output can therefore become mixed.
  11. Synchronization is used to overcome data inconsistency problems.
  12. The document defines synchronization as: at a time only one Thread.
  13. Synchronization coordinates access to shared resources.
  14. Online Reservation Systems are an application area of synchronization.
  15. Funds Transfer from Joint Accounts is another application area mentioned in the document.
  16. The next section implements synchronization using a Lock.

Synchronization Using Lock

Python provides different synchronization mechanisms.

One of the most fundamental synchronization mechanisms provided by the threading module is:

Lock

A Lock is used to make sure that only one Thread can access a particular critical section at a time.

Simple Definition:

A Lock allows only one Thread at a time to execute the protected code.

Multiple Threads
      │
      ▼
     Lock
      │
      ▼
Only One Thread
Gets Permission
      │
      ▼
Critical Section

Why Do We Need Lock?

Without synchronization, multiple Threads may execute the same shared code simultaneously.

This can result in:

  • Irregular output
  • Data inconsistency

A Lock controls access to the shared resource.

Without Lock

Thread-1 ───┐
Thread-2 ───┼──► Shared Resource
Thread-3 ───┘

Possible overlapping execution


With Lock

Thread-1 ───► Lock ───► Shared Resource

Thread-2 ───► Wait

Thread-3 ───► Wait

Creating a Lock Object

We can create a Lock object as follows:

l = Lock()

For this, Lock is imported from the threading module.

from threading import *

l = Lock()

Here:

  • Lock() creates a Lock object.
  • l is the reference variable pointing to the Lock object.
  • The same Lock can be shared by multiple Threads.

How Does a Lock Work?

The Lock object can be held by only one Thread at a time.

If another Thread requires the same Lock, it must wait until the current Thread releases the Lock.

Thread-1
   │
   ▼
Acquire Lock
   │
   ▼
Lock Occupied
   │
   ├──────────── Thread-2 → Wait
   │
   └──────────── Thread-3 → Wait
   │
   ▼
Thread-1 Executes
   │
   ▼
Release Lock
   │
   ▼
One Waiting Thread
Gets the Lock

Real-Life Comparison

The document compares a Lock with examples such as:

  • Common wash rooms
  • Public telephone booths

Consider a public telephone booth.

Person-1
   │
   ▼
Using Booth
   │
   ├──── Person-2 → Wait
   ├──── Person-3 → Wait
   │
   ▼
Person-1 Leaves
   │
   ▼
Next Person Enters

Only one person can use the resource at a time.

A Thread Lock works using the same basic idea.

acquire() Method

A Thread can acquire the Lock by using the acquire() method.

Syntax:

l.acquire()

When a Thread successfully executes:

l.acquire()

that Thread gets the Lock.

Thread
  │
  ▼
l.acquire()
  │
  ▼
Is Lock Available?
  │
  ├──── Yes ───► Acquire Lock
  │
  └──── No ─────► Wait

What Happens if the Lock is Already Acquired?

Suppose Thread-1 already holds the Lock.

Thread-1
   │
   ▼
l.acquire()
   │
   ▼
Lock Acquired

Now Thread-2 executes:

l.acquire()

But the Lock is not available.

Therefore:

Thread-2
   │
   ▼
l.acquire()
   │
   ▼
Lock Already Held
by Thread-1
   │
   ▼
Thread-2 Waits

Thread-2 can continue only after the Lock becomes available.

release() Method

A Thread can release the Lock by using the release() method.

Syntax:

l.release()

Once the Lock is released, another waiting Thread can acquire it.

Thread-1
   │
   ▼
Critical Section
   │
   ▼
l.release()
   │
   ▼
Lock Available
   │
   ▼
Another Waiting Thread
Can Acquire Lock

acquire() and release()

The normal Lock execution pattern is:

l.acquire()

# Critical Section
# Shared Resource Operations

l.release()

The sequence is:

Acquire
   │
   ▼
Execute Protected Code
   │
   ▼
Release
Method Purpose
acquire() Acquire the Lock
release() Release the Lock

Important Rule of release()

The document gives an important rule:

To call release(), the Lock must already be in the acquired state.

If we try to release an unlocked Lock, Python raises:

RuntimeError: release unlocked lock

Therefore, the normal sequence should be:

l.acquire()
     │
     ▼
Lock Acquired
     │
     ▼
l.release()
     │
     ▼
Lock Released

Not:

Lock Not Acquired
      │
      ▼
l.release()
      │
      ▼
RuntimeError

Example - release() Without acquire()

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output When Line-1 is Commented

Program Explanation - release() Error

Step 1: Import threading

from threading import *

The required Threading classes and functions are imported.


Step 2: Create the Lock

l = Lock()

A new Lock object is created.

Initially, the Lock is not acquired.


Step 3: acquire() is Commented

# l.acquire()

Because this statement is commented, the Lock remains unlocked.


Step 4: Call release()

l.release()

Python is asked to release a Lock that is currently unlocked.

Therefore, Python raises:

RuntimeError: release unlocked lock

Execution Flow - release unlocked lock

Program Starts
      │
      ▼
Create Lock
      │
      ▼
Lock is Unlocked
      │
      ▼
l.acquire()
Commented
      │
      ▼
Execute
l.release()
      │
      ▼
Trying to Release
Unlocked Lock
      │
      ▼
RuntimeError:
release unlocked lock

What Happens if Line-1 is Not Commented?

If we execute:

l.acquire()

before:

l.release()

the sequence becomes valid.

from threading import *

l = Lock()

l.acquire()
l.release()

Execution:

Create Lock
    │
    ▼
Acquire Lock
    │
    ▼
Lock Becomes Locked
    │
    ▼
Release Lock
    │
    ▼
Lock Becomes Unlocked
    │
    ▼
Program Ends Normally

Synchronization Example Using Lock

Now the document applies Lock to the earlier wish() synchronization problem.

Three Threads are created:

  • Dhoni
  • Yuvraj
  • Kohli

All three Threads execute the same wish() function.

But before entering the protected code, each Thread must acquire the same Lock.

Complete Program - Synchronization Using Lock

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output

The document does not print the complete output of this program.

It states:

At a time only one Thread is allowed to execute the wish() method and hence we will get regular output.

Conceptually, one possible output order is:

Good Evening:Dhoni
Good Evening:Dhoni
Good Evening:Dhoni
...
10 times

Good Evening:Yuvraj
Good Evening:Yuvraj
Good Evening:Yuvraj
...
10 times

Good Evening:Kohli
Good Evening:Kohli
Good Evening:Kohli
...
10 times

The exact order in which Dhoni, Yuvraj, and Kohli acquire the Lock is not guaranteed.

For example, Yuvraj may acquire the Lock first. The important point is that the outputs of the protected wish() executions do not mix with each other.

Complete Program Explanation

Step 1: Import Required Modules

from threading import *
import time

The threading module provides Thread and Lock functionality.

The time module provides sleep().


Step 2: Create the Lock Object

l = Lock()

A single Lock object is created.

This Lock is shared by all three Threads.

             Lock l
               ▲
          ┌────┼────┐
          │    │    │
          │    │    │
         t1   t2   t3

Step 3: Create the Shared Function

def wish(name):

All three Threads execute the same wish() function.


Step 4: Acquire the Lock

l.acquire()

Before entering the critical section, the Thread must acquire the Lock.

Only one Thread can successfully acquire the Lock at a time.

The remaining Threads wait.


Step 5: Execute the Loop

for i in range(10):

The Thread that owns the Lock executes the loop ten times.


Step 6: Print Good Evening

print("Good Evening:", end='')

The greeting is printed without moving to the next line.


Step 7: Sleep for Two Seconds

time.sleep(2)

The currently executing Thread waits for two seconds.

Even while it is sleeping, it still holds the Lock because release() has not yet been called.

Therefore, the other Threads cannot enter the protected section using this Lock.


Step 8: Print the Name

print(name)

The Thread prints its corresponding name.


Step 9: Complete All Ten Iterations

The Thread continues until all ten iterations are completed.


Step 10: Release the Lock

l.release()

After completing the entire loop, the Thread releases the Lock.

Now one of the waiting Threads can acquire it.


Step 11: Create Three Threads

t1 = Thread(target=wish, args=("Dhoni",))
t2 = Thread(target=wish, args=("Yuvraj",))
t3 = Thread(target=wish, args=("Kohli",))
Thread Target Argument
t1 wish() Dhoni
t2 wish() Yuvraj
t3 wish() Kohli

Step 12: Start All Threads

t1.start()
t2.start()
t3.start()

All three Threads start and request the same Lock.

Only one gets the Lock.

The remaining two wait until the Lock becomes available.

How Three Threads Use One Lock

                 Lock l
                   │
          ┌────────┼────────┐
          │        │        │
          ▼        ▼        ▼
         t1       t2       t3
          │        │        │
          └────────┼────────┘
                   │
                   ▼
             Who Acquires
              Lock First?
                   │
                   ▼
              One Thread
                   │
                   ▼
              wish(name)
                   │
                   ▼
              10 Iterations
                   │
                   ▼
               release()
                   │
                   ▼
             Next Waiting
                Thread

Why Is the Output Regular?

The output becomes regular because:

  • Only one Thread can hold the Lock at a time.
  • The remaining Threads wait until the Lock is released.
  • Only the Lock-owning Thread executes the protected wish() work.
  • The Lock is released only after all ten iterations are completed.

Therefore, we do not get the earlier mixed output such as:

Good Evening:Good Evening:Yuvraj
Dhoni

Instead, each Thread completes its protected execution before another waiting Thread gets the Lock.

Lock is Held During sleep()

An important point in this program is:

l.acquire()

for i in range(10):
    print("Good Evening:", end='')
    time.sleep(2)
    print(name)

l.release()

The Lock is acquired before the loop and released after the loop.

Therefore, even when the current Thread executes:

time.sleep(2)

it does not release the Lock.

Thread-1
   │
   ▼
Acquire Lock
   │
   ▼
Print
   │
   ▼
sleep(2)
   │
   │ Lock Still Held
   │
   ├──── Thread-2 → Waiting
   └──── Thread-3 → Waiting
   │
   ▼
Continue Loop

Execution Flow

Program Starts
      │
      ▼
Create Lock
      │
      ▼
Create t1, t2, t3
      │
      ▼
Start All Threads
      │
      ▼
All Request Same Lock
      │
      ▼
One Thread Acquires Lock
      │
      ├──────────────► Other Thread Waits
      │
      └──────────────► Other Thread Waits
      │
      ▼
Execute wish()
10 Times
      │
      ▼
Release Lock
      │
      ▼
One Waiting Thread
Acquires Lock
      │
      ▼
Execute wish()
10 Times
      │
      ▼
Release Lock
      │
      ▼
Last Waiting Thread
Acquires Lock
      │
      ▼
Execute wish()
10 Times
      │
      ▼
Release Lock
      │
      ▼
All Threads Complete
      │
      ▼
Program Ends

Without Lock vs With Lock

Without Lock With Lock
Multiple Threads may enter shared code simultaneously. Only one Thread can hold the Lock at a time.
Execution overlaps. Protected execution is controlled.
Output can become mixed. Output becomes regular.
Data inconsistency may occur. Data inconsistency can be avoided.
No Thread waits for a Lock. Other Threads wait while the Lock is held.

Lock State

A simple Lock can be understood using two states:

State Meaning
Unlocked The Lock is available to be acquired.
Locked The Lock has already been acquired.
Unlocked
   │
   │ acquire()
   ▼
 Locked
   │
   │ release()
   ▼
Unlocked

Lock Method Comparison

Method Purpose Result
l.acquire() Request the Lock Thread acquires it if available; otherwise it waits
l.release() Release the Lock Lock becomes available

Important Error

Remember this error:

RuntimeError: release unlocked lock

It occurs when:

l = Lock()

l.release()

because the Lock is already unlocked.

The valid order is:

l = Lock()

l.acquire()
l.release()

Summary

  • Lock is a fundamental synchronization mechanism provided by the threading module.
  • A Lock object is created using:
l = Lock()
  • A Lock can be held by only one Thread at a time.
  • If another Thread requires the same Lock, it waits until the current Thread releases it.
  • A Thread acquires the Lock using:
l.acquire()
  • A Thread releases the Lock using:
l.release()
  • Calling release() when the Lock is not acquired raises:
RuntimeError: release unlocked lock
  • Using a Lock ensures that only one Thread executes the protected critical section at a time.
  • This produces regular output in the wish() example.

Important Notes

  1. A Lock object allows only one Thread to hold the Lock at a time.
  2. Other Threads wait until the Lock becomes available.
  3. Use acquire() before entering the critical section.
  4. Use release() after completing the critical section.
  5. The Lock must be in the acquired state before release() is called.
  6. Calling release() on an unlocked Lock results in RuntimeError: release unlocked lock.
  7. In the synchronization example, all three Threads share the same Lock.
  8. The Threads execute the same wish() function.
  9. Thread t1 receives "Dhoni".
  10. Thread t2 receives "Yuvraj".
  11. Thread t3 receives "Kohli".
  12. Only one Thread can execute the Lock-protected wish() work at a time.
  13. The other Threads wait until the Lock is released.
  14. The Lock is acquired before the loop and released after all ten iterations.
  15. Therefore, the Thread continues holding the Lock even during time.sleep(2).
  16. The synchronized program produces regular output instead of the mixed output seen without synchronization.

Problem with Simple Lock

A normal Lock has an important limitation.

The standard Lock object does not care which Thread is currently holding that Lock.

If the Lock is already held and any Thread attempts to acquire the same Lock again, that Thread becomes blocked.

This rule applies even when the Thread trying to acquire the Lock again is the same Thread that already owns it.

Simple Definition:

A normal Lock cannot be acquired again by the same Thread while that Lock is already held.

Main Thread
     │
     ▼
Acquire Lock
     │
     ▼
Lock is Held
     │
     ▼
Main Thread Tries
to Acquire Same Lock
Again
     │
     ▼
Blocked

Demonstration Program - Problem with Simple Lock

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output

What Happens After This Output?

After printing:

Main Thread trying to acquire Lock Again

the program does not continue.

The Main Thread becomes blocked because it is trying to acquire the same normal Lock for the second time.

First acquire()
      │
      ▼
Success
      │
      ▼
Lock Already Held
by Main Thread
      │
      ▼
Second acquire()
      │
      ▼
Main Thread Waits
      │
      ▼
No Thread Releases Lock
      │
      ▼
Program Remains Blocked

Program Explanation

Step 1: Import threading

from threading import *

This imports the required classes and functions from the threading module.


Step 2: Create a Lock Object

l = Lock()

A normal Lock object is created.

Initially, this Lock is available.


Step 3: Print the First Message

print("Main Thread trying to acquire Lock")

The Main Thread displays a message before acquiring the Lock.


Step 4: Acquire the Lock

l.acquire()

The Lock is currently available.

Therefore, the Main Thread successfully acquires it.

Lock Available
     │
     ▼
Main Thread
l.acquire()
     │
     ▼
Lock Acquired

Step 5: Print the Second Message

print("Main Thread trying to acquire Lock Again")

This message is displayed normally.

At this point, the Main Thread still owns the Lock.


Step 6: Acquire the Same Lock Again

l.acquire()

Now the problem occurs.

The Lock is already held.

A normal Lock does not provide a reentrant facility.

Therefore, the Main Thread becomes blocked.

Why Does the Program Stop?

The Main Thread acquired the Lock once:

l.acquire()

Without releasing it, the same Main Thread again executes:

l.acquire()

Because a standard Lock is already in the locked state, the second acquire() waits for the Lock to become available.

But the same Thread is waiting and cannot continue to a future release().

Main Thread
    │
    ▼
Acquire Lock
    │
    ▼
Lock Held
    │
    ▼
Acquire Again
    │
    ▼
Wait for Lock
    │
    ▼
Same Thread Cannot
Continue
    │
    ▼
Blocked

Important Note - Stopping the Blocked Program

The document gives an important command-line note.

To terminate the blocking Thread from the Windows command prompt, use:

Ctrl + Break

The document specifically notes that:

Ctrl + C

does not work for this example in the described environment.

Another Problem with Simple Lock

The problem becomes more important when a Thread works with:

  • Recursive functions
  • Nested access to resources

In such cases, the same Thread may need to acquire the same Lock multiple times.

Thread
  │
  ▼
Function A
  │
  ▼
Acquire Lock
  │
  ▼
Function B
  │
  ▼
Needs Same Lock
  │
  ▼
Acquire Again
  │
  ▼
Blocked with
Normal Lock

Problem with Recursive Functions

A recursive function calls itself.

Suppose a recursive function contains:

l.acquire()

Every recursive call can attempt to acquire the same Lock again.

factorial(3)
     │
     ▼
Acquire Lock
     │
     ▼
factorial(2)
     │
     ▼
Acquire Same Lock Again
     │
     ▼
Blocked with Lock

Therefore, a traditional Lock is not suitable when the same Thread must repeatedly acquire the same Lock during recursion.

Limitation of Traditional Locking

The document concludes:

Traditional Locking mechanism won't work for executing recursive functions.

The reason is simple.

Normal Lock
    │
    ▼
First acquire()
    │
    ▼
Success
    │
    ▼
Same Thread Calls
acquire() Again
    │
    ▼
Blocked

Therefore, we need a synchronization mechanism that allows the owner Thread to acquire the same Lock multiple times.

Solution - RLock

To overcome the limitation of a normal Lock, Python provides:

RLock

RLock means:

Reentrant Lock

Simple Definition:

RLock allows the owner Thread to acquire the same Lock again and again.

If another Thread tries to acquire the RLock while it is held, that other Thread must wait.

What Does Reentrant Mean?

Reentrant means that the Thread which already owns the Lock can enter the same protected locking mechanism again.

Owner Thread
     │
     ▼
Acquire RLock
     │
     ▼
Acquire Same RLock Again
     │
     ▼
Allowed
     │
     ▼
Acquire Again
     │
     ▼
Allowed

This is the main difference between Lock and RLock.

Reentrant Facility

The reentrant facility is available only for the owner Thread.

It is not available to other Threads.

                 RLock
                   │
          Owner = Thread-1
                   │
        ┌──────────┴──────────┐
        │                     │
        ▼                     ▼
Thread-1 acquire()       Thread-2 acquire()
Again                     Same RLock
        │                     │
        ▼                     ▼
     Allowed                Wait

Therefore:

  • The owner Thread can acquire the same RLock multiple times.
  • Another Thread must wait until the RLock is completely released.

Creating an RLock Object

An RLock object can be created using:

l = RLock()

Complete syntax:

from threading import *

l = RLock()

Here:

  • RLock() creates a Reentrant Lock.
  • l refers to that RLock object.

RLock Example

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output

Important Observation

Unlike the previous normal Lock example, the Main Thread does not become blocked on the second acquire().

This is because:

l = RLock()

is used instead of:

l = Lock()

The same owner Thread can acquire an RLock multiple times.

RLock Program Explanation

Step 1: Import threading

from threading import *

The threading functionality is imported.


Step 2: Create RLock

l = RLock()

An RLock or Reentrant Lock object is created.


Step 3: Print the First Message

print("Main Thread trying to acquire Lock")

The Main Thread announces that it is going to acquire the Lock.


Step 4: Acquire RLock

l.acquire()

The Main Thread successfully acquires the RLock.


Step 5: Print the Second Message

print("Main Thread trying to acquire Lock Again")

The Main Thread is about to acquire the same RLock again.


Step 6: Acquire RLock Again

l.acquire()

The same Main Thread already owns the RLock.

Because RLock is reentrant, the second acquisition is allowed.

Therefore, the Main Thread is not blocked.

Lock vs RLock - Same Program

Using Lock

l = Lock()

l.acquire()
l.acquire()

Result:

Second acquire()
      │
      ▼
Blocked

Using RLock

l = RLock()

l.acquire()
l.acquire()

Result:

Second acquire()
      │
      ▼
Allowed for
Owner Thread

Recursion Level in RLock

RLock keeps track of the recursion level.

Every time the owner Thread calls:

l.acquire()

the recursion level increases.

Every time it calls:

l.release()

the recursion level decreases.

The RLock becomes completely available to another Thread only after the required matching release() calls are executed.

Initial Level = 0

acquire()
    │
    ▼
Level = 1

acquire()
    │
    ▼
Level = 2

release()
    │
    ▼
Level = 1

release()
    │
    ▼
Level = 0
    │
    ▼
RLock Completely Released

Matching acquire() and release() Calls

For every acquire() call, a corresponding release() call should be available.

The number of acquisitions and releases should match before the RLock becomes completely released.

Example:

l = RLock()

l.acquire()
l.acquire()

l.release()
l.release()

Here:

Operation Recursion Level
Initial 0
First acquire() 1
Second acquire() 2
First release() 1
Second release() 0

Only after the second release() is the RLock completely released.

Important Notes About Recursion Level

  1. Only the owner Thread can acquire the same RLock multiple times.
  2. The number of acquire() and release() calls should match.
  3. RLock internally keeps track of the recursion level.

Why RLock is Useful for Recursive Functions

Consider a recursive function:

factorial(5)
   │
   ▼
factorial(4)
   │
   ▼
factorial(3)
   │
   ▼
factorial(2)
   │
   ▼
factorial(1)
   │
   ▼
factorial(0)

The same Thread executes all these recursive calls.

If every recursive call needs the same synchronization Lock, a normal Lock creates a problem.

RLock solves this because the same owner Thread is allowed to acquire the same RLock repeatedly.

Demo Program - Synchronization Using RLock

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output

Complete Program Explanation

Step 1: Import Modules

from threading import *
import time

The required threading functionality is imported.


Step 2: Create RLock

l = RLock()

A single RLock object is created.

This RLock is shared during recursive function execution.


Step 3: Define factorial()

def factorial(n):

The factorial() function calculates the factorial of a number recursively.


Step 4: Acquire RLock

l.acquire()

Every call to factorial() attempts to acquire the RLock.


Step 5: Check Base Condition

if n == 0:
    result = 1

When n becomes 0, recursion stops.

The factorial of zero is:

0! = 1

Step 6: Recursive Call

result = n * factorial(n - 1)

The function calls itself.

Because the same Thread executes the recursive call, it attempts to acquire the same RLock again.

RLock permits this.


Step 7: Release RLock

l.release()

Every recursive invocation releases one acquisition of the RLock before returning.

Therefore, every acquire() has a matching release().


Step 8: Return Result

return result

The calculated factorial value is returned.


Step 9: Define results()

def results(n):
    print("The Factorial of", n, "is:", factorial(n))

This function calls factorial() and displays the result.


Step 10: Create Two Threads

t1 = Thread(target=results, args=(5,))
t2 = Thread(target=results, args=(9,))

Two Threads are created.

Thread Function Value
t1 results() 5
t2 results() 9

Step 11: Start Both Threads

t1.start()
t2.start()

Both Threads start executing.

When one Thread owns the RLock, another Thread requesting it must wait.

However, recursive calls made by the owner Thread can acquire the same RLock repeatedly.

How factorial(5) Uses RLock

Conceptually, the recursive acquisition works like this:

factorial(5)
Acquire → Level 1
      │
      ▼
factorial(4)
Acquire → Level 2
      │
      ▼
factorial(3)
Acquire → Level 3
      │
      ▼
factorial(2)
Acquire → Level 4
      │
      ▼
factorial(1)
Acquire → Level 5
      │
      ▼
factorial(0)
Acquire → Level 6
      │
      ▼
Base Case
result = 1

While returning:

factorial(0)
Release → Level 5
      │
      ▼
factorial(1)
Release → Level 4
      │
      ▼
factorial(2)
Release → Level 3
      │
      ▼
factorial(3)
Release → Level 2
      │
      ▼
factorial(4)
Release → Level 1
      │
      ▼
factorial(5)
Release → Level 0
      │
      ▼
RLock Completely Released

Factorial Calculation

For factorial(5):

factorial(5)
= 5 × factorial(4)
= 5 × 4 × factorial(3)
= 5 × 4 × 3 × factorial(2)
= 5 × 4 × 3 × 2 × factorial(1)
= 5 × 4 × 3 × 2 × 1 × factorial(0)
= 5 × 4 × 3 × 2 × 1 × 1
= 120

Therefore:

The Factorial of 5 is: 120

Similarly:

9! = 362880

Important Observation - What if We Use Lock?

The document gives a very important observation:

If we use a normal Lock instead of RLock in this program, the Thread will be blocked.

Suppose we change:

l = RLock()

to:

l = Lock()

Then:

factorial(5)
     │
     ▼
Acquire Lock
     │
     ▼
factorial(4)
     │
     ▼
Same Thread Attempts
to Acquire Same Lock
     │
     ▼
Blocked

This demonstrates why RLock is required for this recursive program.

Execution Flow - RLock Factorial Program

Program Starts
      │
      ▼
Create RLock
      │
      ▼
Create Threads
t1 and t2
      │
      ▼
Start Threads
      │
      ▼
One Thread Acquires
RLock
      │
      ▼
factorial(n)
      │
      ▼
Recursive Function Calls
      │
      ▼
Same Owner Thread
Acquires RLock Again
      │
      ▼
Recursion Continues
      │
      ▼
Base Condition
n == 0
      │
      ▼
Recursive Calls Return
      │
      ▼
Matching release()
Calls Execute
      │
      ▼
Recursion Level
Becomes 0
      │
      ▼
RLock Completely Released
      │
      ▼
Waiting Thread Can
Acquire RLock
      │
      ▼
All Threads Complete
      │
      ▼
Program Ends

Problem with Lock vs Solution with RLock

Situation Lock RLock
First acquisition by Thread Allowed Allowed
Same owner Thread acquires again Blocked Allowed
Different Thread tries while held Blocked Blocked
Recursive function Not suitable Suitable
Nested resource access Not suitable Suitable
Tracks owner Thread No Yes
Tracks recursion level No Yes

Difference Between Lock and RLock

Lock RLock
A Lock object can be acquired by only one Thread at a time. Even the owner Thread cannot acquire the same Lock multiple times. An RLock object can be acquired by only one Thread at a time, but the owner Thread can acquire the same RLock multiple times.
Not suitable for recursive functions and nested access calls. Best suitable for recursive functions and nested access calls.
Lock takes care of whether it is locked or unlocked. RLock takes care of whether it is locked or unlocked and also maintains owner Thread information and recursion level.

Lock and RLock Visual Comparison

Normal Lock

Thread-1
   │
   ▼
acquire()
   │
   ▼
Success
   │
   ▼
acquire() Again
   │
   ▼
BLOCKED

RLock

Thread-1
   │
   ▼
acquire()
   │
   ▼
Success
Level = 1
   │
   ▼
acquire() Again
   │
   ▼
Success
Level = 2
   │
   ▼
release()
Level = 1
   │
   ▼
release()
Level = 0
   │
   ▼
RLock Released

When Should We Use RLock?

RLock is particularly useful when:

  • A recursive function requires synchronization.
  • A Thread may enter the same protected code multiple times.
  • Functions call other synchronized functions that use the same Lock.
  • Nested resource access requires repeated acquisition by the same Thread.
Recursive / Nested Access
          │
          ▼
Same Thread May Need
Same Lock Again
          │
          ▼
Use RLock

Summary

  • A normal Lock cannot be acquired repeatedly by the same owner Thread while it remains locked.
  • If the owner Thread attempts to acquire the normal Lock again, it becomes blocked.
  • This creates problems with recursive functions and nested resource access.
  • Traditional Locking is therefore not suitable for such recursive locking requirements.
  • Python provides RLock to solve this problem.
  • RLock stands for Reentrant Lock.
  • The owner Thread can acquire the same RLock multiple times.
  • Other Threads must wait while the RLock is held.
  • RLock maintains the recursion level.
  • Every acquire() must have a corresponding release().
  • The RLock becomes completely released when the recursion level returns to zero.
  • RLock is suitable for recursive functions and nested resource access.

Important Notes

  1. A standard Lock allows only one acquisition while it remains locked.
  2. Even the owner Thread cannot acquire the same normal Lock again.
  3. Acquiring the same normal Lock twice causes the Thread to become blocked.
  4. The document notes using Ctrl + Break to terminate the blocked Thread from the Windows command prompt in its example environment.
  5. Recursive functions may attempt to acquire the same Lock multiple times.
  6. Nested resource access can also require repeated acquisition of the same Lock.
  7. Traditional Lock is not suitable for such cases.
  8. RLock means Reentrant Lock.
  9. The owner Thread can acquire the same RLock multiple times.
  10. The reentrant facility is available only to the owner Thread.
  11. Other Threads remain blocked while another Thread owns the RLock.
  12. RLock keeps track of the recursion level.
  13. Every acquire() call should have a matching release() call.
  14. For two acquire() calls, two corresponding release() calls are required to completely release the RLock.
  15. RLock is suitable for recursive functions.
  16. RLock is also suitable for nested resource access.
  17. In the factorial example, recursive calls made by the same Thread repeatedly acquire the same RLock.
  18. If a normal Lock is used instead of RLock in the factorial program, the Thread becomes blocked.

What is Semaphore?

In the case of Lock and RLock, only one Thread is allowed to execute the protected code at a time.

Sometimes our requirement is different.

For example:

  • At a time, 10 members may be allowed to access a Database Server.
  • At a time, 4 members may be allowed to use a Network Connection.

For these types of requirements, Lock and RLock are not suitable.

In such situations, we should use:

Semaphore

Simple Definition:

A Semaphore is an advanced synchronization mechanism used to limit access to a shared resource having limited capacity.

Why Do We Need Semaphore?

With a normal Lock:

Shared Resource
      │
      ▼
Only 1 Thread
Allowed

But suppose a Database Server can handle 10 connections simultaneously.

Database Server
      │
      ├── Thread-1
      ├── Thread-2
      ├── Thread-3
      ├── ...
      └── Thread-10

Allowing only one Thread would unnecessarily restrict the resource.

Semaphore allows us to specify exactly how many Threads can access the resource simultaneously.

Real-Life Examples

Example 1: Database Server

Suppose a Database Server allows a maximum of 10 users at the same time.

Semaphore(10)

Maximum Concurrent Access
        =
     10 Threads

Example 2: Network Connection

Suppose only four users are allowed to use a Network Connection simultaneously.

Semaphore(4)

Maximum Concurrent Access
        =
      4 Threads

These requirements can be handled using Semaphore.

Creating a Semaphore Object

A Semaphore object can be created as follows:

s = Semaphore(counter)

Here:

  • s is the Semaphore object reference.
  • counter represents the maximum number of Threads allowed to access the resource simultaneously.

Example:

from threading import *

s = Semaphore(3)

This means a maximum of 3 Threads can acquire the Semaphore at the same time.

Default Value of Semaphore Counter

The default value of the Semaphore counter is:

1

Therefore:

s = Semaphore()

is effectively created with an initial counter of 1.

Only one Thread can acquire it at a time.

Semaphore()
    │
    ▼
Counter = 1
    │
    ▼
Only One Thread
Allowed

Working of acquire() and release()

Semaphore maintains an internal counter.

Whenever a Thread executes:

s.acquire()

the available permit count decreases by 1.

Whenever a Thread executes:

s.release()

the available permit count increases by 1.

Operation Effect on Counter
acquire() Counter decreases by 1
release() Counter increases by 1

Semaphore Counter Example

Suppose we create:

s = Semaphore(3)

Initially:

Counter = 3

Now three Threads acquire it:

Initial Counter = 3
        │
        ▼
Thread-1 acquire()
Counter = 2
        │
        ▼
Thread-2 acquire()
Counter = 1
        │
        ▼
Thread-3 acquire()
Counter = 0

At this point, another Thread cannot acquire a permit immediately.

Thread-4
   │
   ▼
acquire()
   │
   ▼
Counter = 0
   │
   ▼
Wait

When one of the active Threads calls:

s.release()

a permit becomes available and a waiting Thread can proceed.

Case 1 - Default Semaphore

Consider:

s = Semaphore()

No counter value is specified.

Therefore:

Counter = 1

Only one Thread can acquire the Semaphore at a time.

Thread-1
   │
   ▼
Acquire
   │
   ▼
Counter = 0
   │
   ├──── Thread-2 → Wait
   └──── Thread-3 → Wait

This behavior is similar to the basic Lock concept for controlling concurrent access.

Case 2 - Semaphore with Counter

Consider:

s = Semaphore(3)

Here:

Counter = 3

Therefore, three Threads can acquire the Semaphore simultaneously.

Semaphore(3)
      │
 ┌────┼────┐
 ▼    ▼    ▼
T1   T2   T3

Allowed

Any additional Thread must wait until one of these Threads releases a permit.

T4 ───► Wait
T5 ───► Wait

Semaphore Example Program

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Understanding Semaphore(2)

The most important statement in this program is:

s = Semaphore(2)

This means only 2 Threads can acquire the Semaphore simultaneously.

Five Threads are created:

Thread Argument
t1 Dhoni
t2 Yuvraj
t3 Kohli
t4 Rohit
t5 Pandya

But only two of these Threads can execute inside the Semaphore-protected section at the same time.

Program Output

Because Semaphore(2) allows two Threads simultaneously, output from two permitted Threads can be interleaved.

A possible output pattern can look like:

Good Evening:Good Evening:Dhoni
Yuvraj
Good Evening:Good Evening:Dhoni
Yuvraj
...

After one permitted Thread completes its protected work and calls release(), another waiting Thread can enter.

The exact Thread order is not guaranteed because Thread scheduling can vary.

Complete Program Explanation

Step 1: Import Required Modules

from threading import *
import time

The threading module provides Semaphore and Thread functionality.

The time module provides sleep().


Step 2: Create Semaphore

s = Semaphore(2)

The initial counter is 2.

Therefore, up to two Threads can acquire the Semaphore simultaneously.


Step 3: Define wish()

def wish(name):

Every Thread executes the same wish() function.


Step 4: Acquire Semaphore

s.acquire()

Before entering the protected work, each Thread attempts to acquire a Semaphore permit.

If a permit is available, the Thread proceeds.

If no permit is available, the Thread waits.


Step 5: Execute Loop

for i in range(10):

Each permitted Thread executes the loop ten times.


Step 6: Print Greeting

print("Good Evening:", end='')

The greeting is printed without moving to the next line.


Step 7: Sleep

time.sleep(2)

The Thread waits for two seconds.

While this Thread sleeps, another Thread that already holds another Semaphore permit can execute.


Step 8: Print Name

print(name)

The Thread prints its corresponding name.


Step 9: Release Semaphore

s.release()

After completing all ten iterations, the Thread releases its Semaphore permit.

The available counter increases, allowing another waiting Thread to continue.


Step 10: Create Five Threads

t1 = Thread(target=wish, args=("Dhoni",))
t2 = Thread(target=wish, args=("Yuvraj",))
t3 = Thread(target=wish, args=("Kohli",))
t4 = Thread(target=wish, args=("Rohit",))
t5 = Thread(target=wish, args=("Pandya",))

Five Threads are created, but the Semaphore permits only two concurrent acquisitions.


Step 11: Start All Threads

t1.start()
t2.start()
t3.start()
t4.start()
t5.start()

All five Threads request access.

Two Threads get permits.

The remaining three Threads wait.

Execution Flow of Semaphore(2)

Program Starts
      │
      ▼
Create Semaphore(2)
      │
      ▼
Counter = 2
      │
      ▼
Create 5 Threads
      │
      ▼
Start All Threads
      │
      ▼
Thread-1 acquire()
Counter = 1
      │
      ▼
Thread-2 acquire()
Counter = 0
      │
      ├──────── Thread-3 waits
      ├──────── Thread-4 waits
      └──────── Thread-5 waits
      │
      ▼
Two Threads Execute
wish()
      │
      ▼
One Thread Completes
      │
      ▼
release()
      │
      ▼
Permit Becomes Available
      │
      ▼
One Waiting Thread
Acquires Permit
      │
      ▼
Process Continues
      │
      ▼
All Threads Complete

Lock vs RLock vs Semaphore

Feature Lock RLock Semaphore
Concurrent acquisitions 1 1 Thread at a time Fixed number based on counter
Same owner can acquire repeatedly No Yes Controlled by available permits
Main purpose Mutual exclusion Reentrant mutual exclusion Limited-capacity access
Recursive functions Not suitable for repeated acquisition Suitable Not its primary purpose
Example One shared resource Recursive protected operation Limited database connections

BoundedSemaphore

A normal Semaphore allows its internal count to be increased beyond its initial value by extra release() calls.

This can hide programming mistakes.

Python therefore provides:

BoundedSemaphore

BoundedSemaphore works almost like a normal Semaphore.

The important difference is that it checks that releases do not increase the Semaphore beyond its initial bound.

If too many release() calls are made, Python raises:

ValueError: Semaphore released too many times

Normal Semaphore Example

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Normal Semaphore Output

Why Does Normal Semaphore Allow This?

In this program:

Initial Counter = 2

acquire() → 2 times
release() → 4 times

A normal Semaphore permits extra release() calls, so the internal counter can rise above the original value.

Therefore, the program completes and prints:

End

This is why the document describes normal Semaphore as an unlimited Semaphore in this context.

Normal Semaphore Counter Flow

Semaphore(2)
Counter = 2
     │
     ▼
acquire()
Counter = 1
     │
     ▼
acquire()
Counter = 0
     │
     ▼
release()
Counter = 1
     │
     ▼
release()
Counter = 2
     │
     ▼
release()
Counter = 3
     │
     ▼
release()
Counter = 4
     │
     ▼
Print "End"

A normal Semaphore does not enforce the original upper bound when release() is called.

BoundedSemaphore Example

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

BoundedSemaphore Output

Why is the BoundedSemaphore Program Invalid?

The program starts with:

s = BoundedSemaphore(2)

Therefore, its maximum bound is 2.

The program performs:

acquire() → 2 times
release() → 4 times

After two valid releases, the Semaphore has returned to its original bound.

The next extra release attempts to exceed that bound.

Therefore, Python raises:

ValueError: Semaphore released too many times

BoundedSemaphore Execution Flow

BoundedSemaphore(2)
       │
       ▼
Initial Value = 2
       │
       ▼
acquire()
Value = 1
       │
       ▼
acquire()
Value = 0
       │
       ▼
release()
Value = 1
       │
       ▼
release()
Value = 2
       │
       ▼
release() Again
       │
       ▼
Would Exceed
Initial Bound
       │
       ▼
ValueError:
Semaphore released
too many times

Semaphore vs BoundedSemaphore

Semaphore BoundedSemaphore
Controls access using a counter. Also controls access using a counter.
Extra release() calls can increase the counter beyond its initial value. Counter cannot be increased beyond its initial bound.
Extra releases may hide programming mistakes. Extra releases are detected.
Extra release() can be accepted. Extra release() raises ValueError.

Recommendation from the Document

The document recommends using:

BoundedSemaphore

instead of a normal Semaphore when we want to prevent simple programming mistakes involving extra release() calls.

Semaphore
     │
     ▼
Extra release()
May Go Undetected


BoundedSemaphore
     │
     ▼
Extra release()
     │
     ▼
ValueError

Difference Between Lock and Semaphore

Lock Semaphore
Only one Thread can acquire the Lock at a time. A fixed number of Threads, specified by the counter value, can acquire the Semaphore at the same time.

For example:

Lock()
   │
   ▼
1 Thread


Semaphore(3)
   │
   ▼
Up to 3 Threads

Advantage of Synchronization

The main advantage of synchronization is:

Synchronization helps overcome data inconsistency problems.

Multiple Threads
       │
       ▼
Synchronization
       │
       ▼
Controlled Access
       │
       ▼
Reduce Data
Inconsistency Problems

Disadvantage of Synchronization

Synchronization also has disadvantages.

According to the document, synchronization:

  • Increases the waiting time of Threads.
  • Creates performance problems.
Synchronization
      │
      ├──► Threads May Wait
      │
      └──► Performance Overhead

Therefore, if there is no specific requirement for synchronization, it should not be added unnecessarily.

Choosing Lock, RLock or Semaphore

Requirement Suitable Mechanism
Only one Thread should access the resource Lock
Owner Thread must acquire the same Lock repeatedly RLock
Fixed number of Threads should access simultaneously Semaphore
Semaphore should detect excessive releases BoundedSemaphore

Complete Execution Concept

Synchronization
      │
      ├───────────────┐
      │               │
      ▼               ▼
     Lock            RLock
      │               │
      ▼               ▼
One Thread       One Thread
at a Time        at a Time
                      │
                      ▼
                 Owner Can
                 Re-acquire

      Semaphore
          │
          ▼
Fixed Number of
Threads at a Time
          │
          ▼
Semaphore(counter)

      BoundedSemaphore
          │
          ▼
Semaphore with
Release Bound Check

Summary

  • A Semaphore is an advanced synchronization mechanism.
  • It is useful when a shared resource has limited capacity.
  • A Semaphore is created using:
s = Semaphore(counter)
  • The counter specifies how many Threads may acquire the Semaphore simultaneously.
  • The default counter value is 1.
  • Every acquire() decreases the available count by 1.
  • Every release() increases the available count by 1.
  • Semaphore() allows only one Thread at a time.
  • Semaphore(3) permits up to three Threads to acquire it simultaneously.
  • The example uses Semaphore(2) with five Threads.
  • Two Threads can execute the protected wish() work simultaneously while the other three wait.
  • A normal Semaphore permits its count to increase beyond its initial value through extra releases.
  • BoundedSemaphore prevents the count from exceeding its initial bound.
  • Excessive release on BoundedSemaphore raises:
ValueError: Semaphore released too many times
  • Synchronization helps overcome data inconsistency problems.
  • Synchronization also increases waiting time and may affect performance.

Important Notes

  1. Lock and RLock permit only one Thread at a time to hold the synchronization object.
  2. Semaphore is useful when multiple Threads should be allowed simultaneously.
  3. The Semaphore counter specifies the maximum number of simultaneous acquisitions.
  4. The default Semaphore counter is 1.
  5. acquire() decreases the available counter by 1.
  6. release() increases the available counter by 1.
  7. When no permit is available, additional Threads wait.
  8. Semaphore(2) permits two Threads simultaneously.
  9. In the example, five Threads are created: Dhoni, Yuvraj, Kohli, Rohit, and Pandya.
  10. Only two of these Threads can acquire the Semaphore simultaneously.
  11. The remaining Threads wait until a permit becomes available.
  12. A normal Semaphore can accept extra release() calls and its counter can exceed its initial value.
  13. BoundedSemaphore detects releases that would exceed its original bound.
  14. Excessive releases on BoundedSemaphore raise ValueError: Semaphore released too many times.
  15. The document recommends BoundedSemaphore to help prevent simple programming mistakes.
  16. Lock permits one Thread, whereas Semaphore permits a fixed number of Threads according to its counter.
  17. The main advantage of synchronization is avoiding data inconsistency problems.
  18. The disadvantages are increased waiting time and possible performance problems.
  19. Synchronization should be used when there is a specific requirement for controlled shared-resource access.

What is Inter Thread Communication?

Sometimes, as part of a programming requirement, multiple Threads need to communicate with each other.

This communication between Threads is called:

Inter Thread Communication

Simple Definition:

Inter Thread Communication is the mechanism through which one Thread communicates or sends a notification to another Thread.

For example, one Thread may perform some work and another Thread may have to wait until that work is completed.

Producer and Consumer Example

A common example of Inter Thread Communication is the:

Producer - Consumer

Suppose our application contains two Threads:

  • Producer Thread
  • Consumer Thread

The Producer Thread creates new items.

After producing an item, it has to inform the Consumer Thread that an item is available.

Only after receiving this notification can the Consumer Thread consume the item.

Producer Thread
      │
      ▼
Produce Item
      │
      ▼
Send Notification
      │
      ▼
Consumer Thread
      │
      ▼
Receive Notification
      │
      ▼
Consume Item

This communication between Producer and Consumer is called Inter Thread Communication.

Why Do Threads Need Communication?

Consider a Consumer Thread that needs some data.

If the Producer has not produced the data yet, the Consumer should not continue immediately.

Consumer
   │
   ▼
Need Item
   │
   ▼
Item Available?
   │
 ┌─┴─┐
No   Yes
│     │
▼     ▼
Wait  Consume

The Producer should notify the Consumer when the required data becomes available.

Therefore, Threads need a proper communication mechanism.

Ways to Implement Inter Thread Communication

In Python, Inter Thread Communication can be implemented using:

  1. Event
  2. Condition
  3. Queue
  4. etc.
Mechanism Purpose
Event Simple notification between Threads
Condition More advanced waiting and notification mechanism
Queue Advanced mechanism for communication and sharing data

Inter Thread Communication Using Event Object

The Event object is the simplest communication mechanism between Threads.

The basic idea is:

  • One Thread signals an Event.
  • Other Threads wait for that Event.
Thread-1
Producer
   │
   ▼
event.set()
   │
   │ Notification
   ▼
Event Object
   │
   ▼
Thread-2
Consumer
   │
   ▼
Continues

The Event object acts like a signal shared between Threads.

Creating an Event Object

We can create an Event object as follows:

import threading

event = threading.Event()

If everything is imported from the threading module:

from threading import *

event = Event()

Here:

  • Event is provided by the threading module.
  • event refers to the Event object.

Working of Event Object

An Event object internally maintains a flag.

This internal flag can be:

  • Set
  • Cleared

Conceptually:

Event Object
     │
     ▼
Internal Flag
     │
 ┌───┴────┐
 ▼        ▼
False    True
Cleared  Set

Threads can wait until the Event becomes set.

When another Thread sets the Event, waiting Threads are allowed to continue.

Initial State of Event

When an Event object is created, its internal flag is initially in the cleared state.

event = Event()

Internal Flag
     │
     ▼
   False

Therefore, a Thread calling:

event.wait()

waits until another Thread sets the Event.

Methods of Event Class

Method Purpose
set() Sets the internal flag
clear() Clears the internal flag
isSet() Checks whether the Event is set
wait() Waits until the Event becomes set

1. set() Method

The set() method is used as follows:

event.set()

When set() is called:

  • The internal flag becomes True.
  • It acts like a GREEN Signal for waiting Threads.
Before set()

Flag = False
     │
     ▼
event.set()
     │
     ▼
Flag = True
     │
     ▼
GREEN SIGNAL
     │
     ▼
Waiting Threads
Can Continue

2. clear() Method

The clear() method is used as follows:

event.clear()

When clear() is called:

  • The internal flag becomes False.
  • It acts like a RED Signal.
Before clear()

Flag = True
     │
     ▼
event.clear()
     │
     ▼
Flag = False
     │
     ▼
RED SIGNAL

3. isSet() Method

The isSet() method checks whether the Event is currently set.

event.isSet()

Conceptually:

Flag = True
   │
   ▼
event.isSet()
   │
   ▼
True

If the Event is cleared:

Flag = False
   │
   ▼
event.isSet()
   │
   ▼
False

This method is especially useful when a Thread needs to continue working only while the Event remains set.

4. wait() Method

The wait() method is used as follows:

event.wait()

We can also specify a waiting time:

event.wait(seconds)

A Thread calling wait() waits until the Event becomes set.

Thread
  │
  ▼
event.wait()
  │
  ▼
Is Event Set?
  │
 ┌┴─────┐
No      Yes
│        │
▼        ▼
Wait   Continue

GREEN Signal and RED Signal Concept

Operation Flag Meaning
event.set() True GREEN Signal
event.clear() False RED Signal
event.set()
     │
     ▼
GREEN
     │
     ▼
Threads Continue


event.clear()
     │
     ▼
RED
     │
     ▼
Threads Wait

Pseudo Code

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Understanding the Pseudo Code

The Consumer Thread waits using:

event.wait()

The Producer Thread can give a notification using:

event.set()

It can clear the Event using:

event.clear()

Basic flow:

Consumer
   │
   ▼
wait()
   │
   ▼
Waiting
   │
   │
Producer
   │
   ▼
set()
   │
   ▼
Consumer Continues

Demo Program 1 - Producer and Consumer

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Output

Program Explanation - Producer and Consumer

Step 1: Import Required Modules

from threading import *
import time

The threading functionality and time module are imported.


Step 2: Define producer()

def producer():

This function represents the Producer Thread.


Step 3: Producer Sleeps

time.sleep(5)

The Producer waits for five seconds before producing the items.


Step 4: Produce Items

print("Producer thread producing items:")

After five seconds, the Producer starts producing items.


Step 5: Give Notification

event.set()

The Producer sets the Event.

The internal Event flag becomes:

True

This notification allows the waiting Consumer Thread to continue.


Step 6: Define consumer()

def consumer():

This function represents the Consumer Thread.


Step 7: Consumer Displays Waiting Message

print("Consumer thread is waiting for updation")

The Consumer informs us that it is waiting for an update.


Step 8: Consumer Calls wait()

event.wait()

Because the Event has not yet been set, the Consumer enters the waiting state.


Step 9: Consumer Gets Notification

When the Producer executes:

event.set()

the waiting Consumer becomes eligible to continue.


Step 10: Consumer Consumes Items

print("Consumer thread got notification and consuming items")

The Consumer receives the notification and continues its work.

Execution Flow - Producer and Consumer

Program Starts
      │
      ▼
Create Event Object
      │
      ▼
Create Producer Thread
and Consumer Thread
      │
      ▼
Start Both Threads
      │
      ▼
Consumer Starts
      │
      ▼
event.wait()
      │
      ▼
Consumer Enters
Waiting State
      │
      ▼
Producer Sleeps
5 Seconds
      │
      ▼
Producer Produces
Items
      │
      ▼
Producer Calls
event.set()
      │
      ▼
Event Flag = True
      │
      ▼
Waiting Consumer
Gets Notification
      │
      ▼
Consumer Continues
      │
      ▼
Consumes Items
      │
      ▼
Program Completes

Important Point in Producer-Consumer Program

The important relationship is:

Consumer
   │
   ▼
event.wait()


Producer
   │
   ▼
event.set()

The Consumer is the Thread waiting for an update.

The Producer performs the update and sends the notification.

Therefore:

Producer → set()

Consumer → wait()

Demo Program 2 - Traffic Signal Example

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Traffic Signal Program - Sample Output

The program runs continuously, so there is no single final output.

A sample pattern is:

Drivers waiting for GREEN Signal
Traffic Police Giving GREEN Signal
Traffic Signal is GREEN. Vehicles can move
Vehicle No: 1 Crossing the Signal
Vehicle No: 2 Crossing the Signal
Vehicle No: 3 Crossing the Signal
...
Traffic Police Giving RED Signal
Traffic Signal is RED. Drivers have to wait
Drivers waiting for GREEN Signal
...

The exact number of vehicles printed during each GREEN period depends on Thread scheduling and timing.

Traffic Signal Program Explanation

This program uses an Event object like a real traffic signal.

There are two Threads:

Thread Responsibility
Traffic Police Thread Changes signal between GREEN and RED
Driver Thread Waits for GREEN and allows vehicles to cross

Traffic Police Thread

The Traffic Police executes:

def trafficpolice():
    while True:

It works continuously.

First it waits for 10 seconds:

time.sleep(10)

Then it gives a GREEN signal:

print("Traffic Police Giving GREEN Signal")
event.set()

The Event flag becomes:

True

After that, the Traffic Police waits for 20 seconds:

time.sleep(20)

Then it gives a RED signal:

print("Traffic Police Giving RED Signal")
event.clear()

The Event flag becomes:

False

Traffic Police Signal Flow

Traffic Police
      │
      ▼
Wait 10 Seconds
      │
      ▼
event.set()
      │
      ▼
GREEN SIGNAL
Flag = True
      │
      ▼
Wait 20 Seconds
      │
      ▼
event.clear()
      │
      ▼
RED SIGNAL
Flag = False
      │
      ▼
Repeat

Driver Thread

The Driver Thread starts with:

num = 0

This variable is used to generate vehicle numbers.

The Driver continuously executes:

while True:

First, drivers wait for the GREEN signal:

print("Drivers waiting for GREEN Signal")
event.wait()

If the Event flag is False, the Driver Thread waits.

When the Traffic Police calls:

event.set()

the Event becomes set and the Driver continues.

Vehicles Crossing During GREEN Signal

After receiving the GREEN signal:

print("Traffic Signal is GREEN. Vehicles can move")

The Driver checks:

while event.isSet():

As long as the Event remains set, vehicles continue crossing.

num = num + 1

print("Vehicle No:", num, "Crossing the Signal")

time.sleep(2)

Every two seconds, another vehicle is shown crossing the signal.

Event Set?
   │
   ▼
 True
   │
   ▼
Vehicle Crosses
   │
   ▼
Wait 2 Seconds
   │
   └──────────────┐
                  │
                  ▼
             Check Again

What Happens When Signal Becomes RED?

The Traffic Police eventually executes:

event.clear()

The Event flag becomes:

False

Therefore:

while event.isSet():

becomes False.

The Driver exits the inner loop and executes:

print("Traffic Signal is RED. Drivers have to wait")

Then the outer loop repeats and the Driver again reaches:

event.wait()

Now it waits for the next GREEN signal.

Complete Traffic Signal Execution Flow

Program Starts
      │
      ▼
Create Event
      │
      ▼
Create Traffic Police
and Driver Threads
      │
      ▼
Start Both Threads
      │
      ▼
Driver Calls
event.wait()
      │
      ▼
Driver Waits
      │
      ▼
Traffic Police
Waits 10 Seconds
      │
      ▼
event.set()
      │
      ▼
GREEN Signal
      │
      ▼
Driver Continues
      │
      ▼
Vehicles Cross
      │
      ▼
event.isSet()
Returns True
      │
      ▼
Traffic Police
Waits 20 Seconds
      │
      ▼
event.clear()
      │
      ▼
RED Signal
      │
      ▼
event.isSet()
Returns False
      │
      ▼
Vehicles Stop
      │
      ▼
Driver Calls
event.wait() Again
      │
      ▼
Process Repeats

Event Flag State Diagram

             Event Object
                  │
                  ▼
             Flag = False
                  │
                  │ event.set()
                  ▼
             Flag = True
                  │
                  │ event.clear()
                  ▼
             Flag = False
Flag Signal Effect
False RED Threads calling wait() remain waiting
True GREEN Waiting Threads can continue

set(), clear(), isSet() and wait() Comparison

Method Purpose Effect
set() Set Event Flag becomes True
clear() Clear Event Flag becomes False
isSet() Check Event Returns whether Event is set
wait() Wait for Event Blocks until Event is set
wait(seconds) Wait with timeout Waits for Event or specified timeout

Producer-Consumer vs Traffic Signal Example

Concept Producer-Consumer Traffic Signal
Notifier Producer Traffic Police
Waiting Thread Consumer Driver
set() Items available GREEN Signal
clear() Can clear notification state RED Signal
wait() Consumer waits for update Driver waits for GREEN
isSet() Check Event state Check whether signal is still GREEN

Summary

  • Multiple Threads sometimes need to communicate with each other.
  • This communication is called Inter Thread Communication.
  • The Producer–Consumer problem is a common example.
  • The Producer creates an item and notifies the Consumer.
  • The Consumer waits for notification before consuming the item.
  • Python provides several Inter Thread Communication mechanisms:
Event
Condition
Queue
  • The Event object is the simplest communication mechanism.
  • An Event internally maintains a flag.
  • set() makes the flag True.
  • set() acts like a GREEN signal.
  • clear() makes the flag False.
  • clear() acts like a RED signal.
  • isSet() checks whether the Event is currently set.
  • wait() makes a Thread wait until the Event becomes set.
  • wait(seconds) can be used with a waiting time.
  • In the Producer–Consumer example, the Consumer waits and the Producer sets the Event.
  • In the Traffic Signal example, Traffic Police controls the Event and Drivers react to its state.

Important Notes

  1. Inter Thread Communication allows Threads to coordinate with each other.
  2. Producer and Consumer is the basic example used to understand Thread communication.
  3. Python supports Event, Condition, Queue, etc. for Inter Thread Communication.
  4. Event is the simplest mechanism among the mechanisms introduced here.
  5. An Event object maintains an internal flag.
  6. event.set() changes the flag to True.
  7. event.clear() changes the flag to False.
  8. event.set() works like a GREEN signal.
  9. event.clear() works like a RED signal.
  10. event.isSet() checks whether the Event is set.
  11. event.wait() blocks the Thread until the Event is set.
  12. event.wait(seconds) supports waiting with a timeout.
  13. In the Producer–Consumer example, the Consumer calls wait().
  14. The Producer calls set() after producing the items.
  15. In the Traffic Signal example, Traffic Police calls set() for GREEN.
  16. Traffic Police calls clear() for RED.
  17. Drivers use wait() to wait for GREEN.
  18. Drivers use isSet() to continue crossing while the signal remains GREEN.

What is a Condition Object?

A Condition object is a more advanced mechanism than an Event object for Inter Thread Communication.

A Condition represents a state change in an application.

Examples:

  • Producing an item
  • Consuming an item

Threads can wait for a particular condition.

When that condition occurs, another Thread can notify the waiting Threads.

Simple Definition:

A Condition object allows one or more Threads to wait until they are notified by another Thread.

Basic Condition Concept

Consider two Threads:

  • Producer Thread
  • Consumer Thread

The Consumer needs an item from the Producer.

If the item is not available, the Consumer should wait.

After producing the item, the Producer sends a notification.

Consumer Thread
      │
      ▼
Wait for Item
      │
      ▼
condition.wait()
      │
      ▼
   Waiting
      │
      │
      │ Notification
      │
Producer Thread
      │
      ▼
Produce Item
      │
      ▼
condition.notify()
      │
      ▼
Consumer Continues
      │
      ▼
Consume Item

Condition and Lock

A Condition object is always associated with a Lock.

The document describes this as a Reentrant Lock.

The Condition object internally uses this Lock for synchronization.

Condition Object
       │
       ▼
Internal Lock
(Reentrant Lock)
       │
       ├── acquire()
       │
       └── release()

Therefore, a Thread should normally acquire the Condition before performing operations such as:

  • Producing an item
  • Consuming an item
  • Calling wait()
  • Calling notify()

Creating a Condition Object

We can create a Condition object as follows:

import threading

condition = threading.Condition()

If everything is imported from the threading module:

from threading import *

condition = Condition()

Here:

  • Condition is the predefined class.
  • condition is the reference variable.

Methods of Condition Object

Method Purpose
acquire() Acquires the internal Lock
release() Releases the internal Lock
wait() Waits until notification
notify() Notifies one waiting Thread
notifyAll() Notifies all waiting Threads

1. acquire() Method

The acquire() method is used as follows:

condition.acquire()

Purpose:

  • Acquire the Condition object before producing or consuming items.
  • The Thread acquires the internal Lock associated with the Condition.
Thread
  │
  ▼
condition.acquire()
  │
  ▼
Acquire Internal Lock
  │
  ▼
Enter Protected Work

2. release() Method

The release() method is used as follows:

condition.release()

Purpose:

  • Release the Condition after producing or consuming an item.
  • The Thread releases the internal Lock.
Protected Work
      │
      ▼
condition.release()
      │
      ▼
Internal Lock Released

3. wait() Method

The wait() method is used as follows:

condition.wait()

We can also specify a waiting time:

condition.wait(time)

The Thread waits until:

  • Another Thread sends a notification, or
  • The specified waiting time expires.
condition.wait()
       │
       ▼
Thread Waits
       │
       ▼
Notification Received
       │
       ▼
Thread Continues

4. notify() Method

The notify() method is used as follows:

condition.notify()

Its purpose is to notify one waiting Thread.

Waiting Threads

T1   T2   T3
│    │    │
└────┼────┘
     │
     ▼
condition.notify()
     │
     ▼
One Waiting Thread
Gets Notification

5. notifyAll() Method

The notifyAll() method is used as follows:

condition.notifyAll()

Its purpose is to notify all waiting Threads.

Waiting Threads

T1   T2   T3
│    │    │
└────┼────┘
     │
     ▼
condition.notifyAll()
     │
     ▼
Notify All
Waiting Threads

notify() vs notifyAll()

notify() notifyAll()
Notifies one waiting Thread. Notifies all waiting Threads.
Useful when only one waiting Thread needs to continue. Useful when all waiting Threads need notification.

Case Study - Producer Thread

The Producer Thread should perform the following operations:

  1. Produce an item.
  2. Acquire the Condition.
  3. Add the item to the shared resource.
  4. Notify waiting Consumers.
  5. Release the Condition.
Producer
   │
   ▼
Generate Item
   │
   ▼
Acquire Condition
   │
   ▼
Add Item
   │
   ▼
Notify Consumer
   │
   ▼
Release Condition

Producer Thread - Pseudo Code

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Case Study - Consumer Thread

The Consumer Thread should:

  1. Acquire the Condition.
  2. Wait for notification.
  3. Consume the item.
  4. Release the Condition.
Consumer
   │
   ▼
Acquire Condition
   │
   ▼
Wait
   │
   ▼
Receive Notification
   │
   ▼
Consume Item
   │
   ▼
Release Condition

Consumer Thread - Pseudo Code

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Producer vs Consumer Responsibilities

Producer Consumer
Produces item Consumes item
Acquires Condition Acquires Condition
Updates shared resource Waits for update
Calls notify() or notifyAll() Calls wait()
Releases Condition Releases Condition

Demo Program 1 - Simple Producer and Consumer

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Demo Program 1 - Output

Demo Program 1 - Complete Explanation

Step 1: Import threading

from threading import *

This imports Thread and Condition functionality.


Step 2: Define consume()

def consume(c):

The consume() function represents the Consumer Thread.


Step 3: Consumer Acquires Condition

c.acquire()

The Consumer acquires the Condition's internal Lock.


Step 4: Consumer Waits

print("Consumer waiting for updation")
c.wait()

The Consumer now waits until another Thread sends a notification.


Step 5: Define produce()

def produce(c):

This function represents the Producer Thread.


Step 6: Producer Acquires Condition

c.acquire()

The Producer acquires the Condition.


Step 7: Producer Produces Items

print("Producer Producing Items")

The Producer performs its update.


Step 8: Producer Gives Notification

print("Producer giving Notification")
c.notify()

notify() sends a notification to one waiting Thread.

Here, the waiting Consumer receives the notification.


Step 9: Producer Releases Condition

c.release()

The Producer releases the internal Lock.


Step 10: Consumer Continues

print("Consumer got notification & consuming the item")

After receiving the notification, the Consumer continues and consumes the item.


Step 11: Consumer Releases Condition

c.release()

Finally, the Consumer releases the Condition.

Demo Program 1 - Execution Flow

Program Starts
      │
      ▼
Create Condition
      │
      ▼
Create Consumer Thread
and Producer Thread
      │
      ▼
Start Consumer
      │
      ▼
c.acquire()
      │
      ▼
Consumer Calls
c.wait()
      │
      ▼
Consumer Waits
      │
      ▼
Producer Executes
      │
      ▼
c.acquire()
      │
      ▼
Produce Items
      │
      ▼
c.notify()
      │
      ▼
Consumer Gets
Notification
      │
      ▼
Producer Releases
Condition
      │
      ▼
Consumer Continues
      │
      ▼
Consume Item
      │
      ▼
Consumer Releases
Condition

How wait() Works with the Condition Lock

An important concept is that the Consumer initially acquires the Condition:

c.acquire()

and then calls:

c.wait()

While waiting, the Condition allows the internal Lock to become available so another Thread can acquire it and perform the update.

Otherwise, the Producer would never be able to acquire the Condition.

Consumer acquires Condition
          │
          ▼
       c.wait()
          │
          ▼
Wait for Notification
and allow Producer
to use Condition
          │
          ▼
Producer acquires
          │
          ▼
Producer notifies
          │
          ▼
Consumer resumes

Demo Program 2 - Producer and Consumer with Items

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Demo Program 2 - Sample Output

Demo Program 2 - Program Structure

The second example performs actual data sharing between the Producer and Consumer.

The shared resource is:

items = []

The Producer:

  • Generates a random item.
  • Adds it to items.
  • Sends a notification.

The Consumer:

  • Waits for notification.
  • Removes an item.
  • Consumes it.

Producer Thread - Detailed Explanation

The Producer executes continuously:

while True:

First, it acquires the Condition:

c.acquire()

It generates a random number:

item = random.randint(1,100)

For example:

49

The generated item is displayed:

print("Producer Producing Item:", item)

Then the item is added to the shared list:

items.append(item)

The Producer gives notification:

c.notify()

Finally, it releases the Condition:

c.release()

and waits for five seconds:

time.sleep(5)

Consumer Thread - Detailed Explanation

The Consumer also executes continuously:

while True:

It acquires the Condition:

c.acquire()

Then it displays:

Consumer waiting for updation

and waits:

c.wait()

After receiving notification from the Producer, the Consumer removes an item:

items.pop()

The consumed item is displayed:

print("Consumer consumed the item", items.pop())

Finally:

c.release()
time.sleep(5)

The Consumer releases the Condition and waits for five seconds.

How the Shared List Changes

Suppose the Producer generates:

49

Initially:

items = []

Producer executes:

items.append(49)

Now:

items = [49]

The Producer calls:

c.notify()

The Consumer receives the notification and executes:

items.pop()

Now:

items = []

Conceptually:

Producer
   │
   ▼
Generate 49
   │
   ▼
items.append(49)
   │
   ▼
[49]
   │
   ▼
notify()
   │
   ▼
Consumer
   │
   ▼
items.pop()
   │
   ▼
49 Consumed
   │
   ▼
[]

Demo Program 2 - Execution Flow

Program Starts
      │
      ▼
items = []
      │
      ▼
Create Condition
      │
      ▼
Create Producer and
Consumer Threads
      │
      ▼
Consumer Acquires
Condition
      │
      ▼
Consumer Calls wait()
      │
      ▼
Consumer Waits
      │
      ▼
Producer Acquires
Condition
      │
      ▼
Generate Random Item
      │
      ▼
items.append(item)
      │
      ▼
c.notify()
      │
      ▼
Release Condition
      │
      ▼
Consumer Receives
Notification
      │
      ▼
items.pop()
      │
      ▼
Consume Item
      │
      ▼
Release Condition
      │
      ▼
Sleep
      │
      ▼
Process Repeats

Important Note - Who Calls wait() and notify()?

The document gives an important rule for understanding the Producer–Consumer program.

The Consumer Thread is waiting for an update.

Therefore, the Consumer is responsible for calling:

wait()

The Producer Thread performs the update.

Therefore, the Producer is responsible for calling:

notify()

or:

notifyAll()

on the Condition object.

Consumer
   │
   ▼
wait()


Producer
   │
   ├── notify()
   │
   └── notifyAll()

Condition vs Event

Event Condition
Simpler communication mechanism More advanced communication mechanism
Internally maintains a flag Associated with a Lock
Uses set() Uses notify()
Uses clear() Uses Lock-based synchronization
Uses wait() Uses wait()
Useful for signal-style communication Useful for waiting for application state changes

Condition Method Comparison

Method Executed By Purpose
acquire() Producer / Consumer Acquire internal Lock
release() Producer / Consumer Release internal Lock
wait() Usually Consumer Wait for an update
notify() Usually Producer Notify one waiting Thread
notifyAll() Usually Producer Notify all waiting Threads

Complete Condition Communication Flow

              Condition
                  │
                  ▼
             Internal Lock
                  │
        ┌─────────┴─────────┐
        │                   │
        ▼                   ▼
     Producer            Consumer
        │                   │
        ▼                   ▼
    acquire()            acquire()
        │                   │
        │                   ▼
        │                wait()
        │                   │
        ▼                   ▼
 Produce Item            Waiting
        │                   ▲
        ▼                   │
 Add to Resource            │
        │                   │
        ▼                   │
    notify() ────────────────┘
        │
        ▼
    release()
                            │
                            ▼
                       Consume Item
                            │
                            ▼
                        release()

Summary

  • A Condition is a more advanced Inter Thread Communication mechanism than Event.
  • A Condition represents a state change such as producing or consuming an item.
  • One or more Threads can wait until another Thread sends a notification.
  • A Condition object is associated with a Lock.
  • The document describes the associated Lock as a Reentrant Lock.
  • acquire() acquires the Condition's internal Lock.
  • release() releases the internal Lock.
  • wait() blocks the Thread until notification or timeout.
  • notify() wakes one waiting Thread.
  • notifyAll() wakes all waiting Threads.
  • The Producer generally performs the update.
  • The Producer generally calls notify() or notifyAll().
  • The Consumer generally waits for an update.
  • The Consumer generally calls wait().
  • In the second program, the Producer generates a random number and adds it to the items list.
  • The Consumer removes the produced item using items.pop().

Important Notes

  1. Condition is more advanced than Event for Inter Thread Communication.
  2. A Condition object allows one or more Threads to wait for notification.
  3. Condition internally uses a Lock for synchronization.
  4. Use acquire() before performing protected Condition operations.
  5. Use release() after completing the operation.
  6. wait() waits until another Thread sends a notification or the specified waiting time expires.
  7. notify() notifies one waiting Thread.
  8. notifyAll() notifies all waiting Threads.
  9. The Consumer generally calls wait() because it is waiting for an update.
  10. The Producer generally calls notify() or notifyAll() because it performs the update.
  11. Both Producer and Consumer acquire and release the Condition.
  12. In Demo Program 1, the Consumer waits until the Producer sends a notification.
  13. In Demo Program 2, the Producer generates random items between 1 and 100.
  14. The Producer stores generated items using items.append(item).
  15. The Consumer consumes an item using items.pop().
  16. The Producer and Consumer programs demonstrate synchronization together with communication.

Inter Thread Communication Using Queue

The Queue concept is the most advanced mechanism for Inter Thread Communication and for sharing data between Threads.

In the previous topics, we used:

  • Event
  • Condition

With Condition, the programmer has to manually manage operations such as:

acquire()
wait()
notify()
release()

Queue makes this process easier because synchronization is handled automatically.

Simple Definition:

Queue is an Inter Thread Communication mechanism that allows Threads to safely share data with automatic synchronization.

Why Queue?

A Queue internally contains:

  • Condition
  • Lock

Because of this, whenever we use a Queue, we do not need to worry about synchronization manually.

              Queue
                │
        ┌───────┴───────┐
        │               │
        ▼               ▼
    Condition          Lock
        │               │
        └───────┬───────┘
                ▼
      Automatic Synchronization

The Queue module automatically handles synchronization for us.

Queue in Producer-Consumer Model

Queue is very useful in the Producer–Consumer model.

The Producer creates an item and inserts it into the Queue.

The Consumer removes the item from the Queue and consumes it.

Producer Thread
      │
      ▼
Produce Item
      │
      ▼
q.put(item)
      │
      ▼
   ┌───────┐
   │ Queue │
   └───────┘
      │
      ▼
q.get()
      │
      ▼
Consumer Thread
      │
      ▼
Consume Item

Importing the Queue Module

Before using Queue, we should import the queue module.

import queue

The Queue class is available inside this module.

Creating a Queue Object

We can create a Queue object as follows:

q = queue.Queue()

Here:

  • queue is the module.
  • Queue() creates the Queue object.
  • q is the reference variable.
import queue

q = queue.Queue()

Important Methods of Queue

The document introduces two important Queue methods:

Method Purpose
put() Inserts an item into the Queue
get() Removes and returns an item from the Queue
Producer → put()

Consumer → get()

1. put() Method

The put() method is used to insert an item into the Queue.

Syntax:

q.put(item)

Example:

q.put(10)

This inserts 10 into the Queue.

Before

Queue
┌──────────────┐
│              │
└──────────────┘

q.put(10)

After

Queue
┌──────────────┐
│      10      │
└──────────────┘

The Producer Thread generally uses put().

Working of put()

The Producer Thread uses:

q.put(item)

When put() is executed, Queue automatically performs synchronization.

Conceptually:

Producer Thread
      │
      ▼
q.put(item)
      │
      ▼
Acquire Lock Internally
      │
      ▼
Insert Item
      │
      ▼
Release Lock Automatically
      │
      ▼
Continue

The programmer does not need to manually write:

lock.acquire()

# insert item

lock.release()

Queue handles these operations automatically.

What Happens if the Queue is Full?

The put() method also checks whether the Queue is full.

If the Queue is full:

  • The Producer Thread automatically enters the waiting state.
  • Internally, waiting is handled automatically.
  • The Producer continues only after space becomes available.
Producer
   │
   ▼
q.put(item)
   │
   ▼
Is Queue Full?
   │
 ┌─┴───────┐
 │         │
No        Yes
 │         │
 ▼         ▼
Insert     Wait
Item       │
           ▼
      Space Available
           │
           ▼
       Insert Item

Producer Waiting When Queue is Full

Suppose we have a limited Queue:

q = queue.Queue(2)

Conceptually, it can contain only two items at a time.

Queue Capacity = 2

┌─────────┬─────────┐
│ Item-1  │ Item-2  │
└─────────┴─────────┘

Queue Full

If the Producer tries:

q.put(Item-3)

the Producer waits until the Consumer removes an existing item.

Producer
   │
   ▼
put(Item-3)
   │
   ▼
Queue Full
   │
   ▼
WAIT
   │
   │ Consumer removes Item-1
   ▼
Space Available
   │
   ▼
Insert Item-3

2. get() Method

The get() method removes an item from the Queue and returns that item.

Syntax:

q.get()

Normally we store the returned item:

item = q.get()

Example:

Queue

┌──────────────┐
│      10      │
└──────────────┘

item = q.get()

item = 10

Queue

┌──────────────┐
│              │
└──────────────┘

The Consumer Thread generally uses get().

Working of get()

The Consumer Thread uses:

item = q.get()

When get() executes:

  • The Lock is acquired internally.
  • An item is removed from the Queue.
  • The removed item is returned.
  • The Lock is released automatically.
Consumer Thread
      │
      ▼
q.get()
      │
      ▼
Acquire Lock Internally
      │
      ▼
Remove Item
      │
      ▼
Return Item
      │
      ▼
Release Lock Automatically

Again, no manual synchronization is required.

What Happens if the Queue is Empty?

If the Queue is empty, there is no item available for the Consumer.

In this situation:

  • The Consumer Thread automatically enters the waiting state.
  • Waiting is handled internally.
  • When the Producer inserts a new item, the waiting Consumer is notified automatically.
Consumer
   │
   ▼
q.get()
   │
   ▼
Is Queue Empty?
   │
 ┌─┴───────┐
 │         │
No        Yes
 │         │
 ▼         ▼
Remove     Wait
Item       │
           │
       Producer
       Adds Item
           │
           ▼
        Wake Up
           │
           ▼
      Remove Item

Consumer Waiting Example

Initially:

Queue

┌──────────────┐
│    EMPTY     │
└──────────────┘

The Consumer executes:

item = q.get()

Because the Queue is empty:

Consumer
   │
   ▼
Waiting

Later, the Producer executes:

q.put(49)

Now:

Producer
   │
   ▼
put(49)
   │
   ▼
Queue Gets Item
   │
   ▼
Waiting Consumer
Automatically Continues
   │
   ▼
get()
   │
   ▼
item = 49

Automatic Synchronization

The biggest advantage of Queue is:

The queue module takes care of locking automatically.

With Queue, we do not manually write:

acquire()
release()
wait()
notify()

Queue handles the required locking, waiting, and notification internally.

Without Queue
     │
     ├── acquire()
     ├── wait()
     ├── notify()
     └── release()


With Queue
     │
     ├── put()
     └── get()

Synchronization
Handled Automatically

Complete Producer-Consumer Program Using Queue

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Sample Output

Important Note About Output

The program contains:

while True:

in both the Producer and Consumer functions.

Therefore, the program continues producing and consuming items repeatedly.

The random values can also change every time because the Producer uses:

random.randint(1,100)

So values such as 49 and 82 are only sample values.

Complete Program Explanation

Step 1: Import threading

from threading import *

This provides the Thread class.


Step 2: Import time

import time

The time module provides sleep().


Step 3: Import random

import random

The Producer uses this module to generate random items.


Step 4: Import queue

import queue

This module provides the Queue class.


Step 5: Define produce()

def produce(q):

The Queue object is passed to the Producer function.


Step 6: Run Producer Continuously

while True:

The Producer continuously creates new items.


Step 7: Generate Random Item

item = random.randint(1,100)

A random integer between 1 and 100 is generated.


Step 8: Display Produced Item

print("Producer Producing Item:", item)

The generated item is displayed.


Step 9: Insert Item into Queue

q.put(item)

The Producer inserts the item into the Queue.

Queue handles the required synchronization automatically.


Step 10: Producer Notification Message

print("Producer giving Notification")

The program displays a message indicating that a new item is available.

The actual Queue coordination is handled internally by put() and get(); the programmer does not explicitly call notify().


Step 11: Producer Sleeps

time.sleep(5)

The Producer waits five seconds before producing the next item.

Consumer Thread - Complete Explanation

Step 1: Define consume()

def consume(q):

The same Queue object is passed to the Consumer.


Step 2: Run Continuously

while True:

The Consumer continuously waits for and consumes items.


Step 3: Display Waiting Message

print("Consumer waiting for updation")

The Consumer displays that it is waiting for an item.


Step 4: Get Item

item = q.get()

This is the most important Consumer operation.

If an item is available:

q.get()
   │
   ▼
Remove Item
   │
   ▼
Return Item

If the Queue is empty:

q.get()
   │
   ▼
Queue Empty
   │
   ▼
Consumer Waits

Step 5: Consume Item

print("Consumer consumed the item", item)

The item removed from the Queue is displayed.


Step 6: Consumer Sleeps

time.sleep(5)

The Consumer waits five seconds before repeating the process.

Creating the Queue

The Queue object is created using:

q = queue.Queue()

The same Queue object is shared by both Threads.

               Queue q
                  ▲
                  │
          ┌───────┴───────┐
          │               │
          │               │
      Producer         Consumer
          │               │
       q.put()          q.get()

Creating Producer and Consumer Threads

The Producer Thread is created using:

t1 = Thread(target=produce, args=(q,))

The Consumer Thread is created using:

t2 = Thread(target=consume, args=(q,))

Both receive the same Queue object:

             q
        ┌────┴────┐
        ▼         ▼
       t1        t2
    Producer   Consumer

Starting the Threads

Both Threads are started using:

t1.start()
t2.start()

After starting, Producer and Consumer execute independently.

The Queue coordinates communication between them.

Producer Execution Flow

Producer Starts
      │
      ▼
Generate Random Item
      │
      ▼
Print Produced Item
      │
      ▼
q.put(item)
      │
      ▼
Queue Stores Item
      │
      ▼
Print Notification
      │
      ▼
Sleep 5 Seconds
      │
      ▼
Repeat

Consumer Execution Flow

Consumer Starts
      │
      ▼
Print Waiting Message
      │
      ▼
q.get()
      │
      ▼
Is Item Available?
      │
   ┌──┴───┐
   │      │
  Yes     No
   │      │
   ▼      ▼
Remove   Wait
Item      │
   │      │
   │   Producer
   │   Inserts Item
   │      │
   └──────┘
      │
      ▼
Print Consumed Item
      │
      ▼
Sleep 5 Seconds
      │
      ▼
Repeat

Complete Producer-Consumer Execution Flow

Program Starts
      │
      ▼
Create Queue
      │
      ▼
Create Producer
and Consumer Threads
      │
      ▼
Start Both Threads
      │
      ▼
Consumer Calls q.get()
      │
      ▼
Queue Empty?
      │
      ▼
Consumer Waits
      │
      ▼
Producer Generates Item
      │
      ▼
Producer Calls q.put(item)
      │
      ▼
Item Added to Queue
      │
      ▼
Waiting Consumer
Can Continue
      │
      ▼
Consumer q.get()
Returns Item
      │
      ▼
Consumer Processes Item
      │
      ▼
Both Sleep
      │
      ▼
Process Repeats

How Queue Automatically Handles Synchronization

Suppose Producer and Consumer try to access the Queue at the same time.

The programmer does not need to write synchronization code manually.

Producer
   │
   ▼
q.put()
   │
   ├───────────────┐
   │               │
   ▼               │
Queue Internal     │
Synchronization    │
   ▲               │
   │               │
   └───────────────┤
                   │
Consumer           │
   │               │
   ▼               │
q.get() ───────────┘

The Queue implementation safely coordinates these operations.

Queue Full vs Queue Empty

Situation Affected Thread Behaviour
Queue has space Producer put() inserts the item
Queue is full Producer put() waits automatically
Queue contains an item Consumer get() removes and returns the item
Queue is empty Consumer get() waits automatically

Advantages of Queue

Using Queue provides the following benefits:

  • Easy Inter Thread Communication.
  • Automatic synchronization.
  • No need to manually use Lock.
  • No need to manually use Condition.
  • Producer automatically waits if the Queue is full.
  • Consumer automatically waits if the Queue is empty.
Queue
 │
 ├── Easy Communication
 │
 ├── Automatic Synchronization
 │
 ├── Automatic Locking
 │
 ├── Automatic Waiting
 │
 └── Automatic Notification

Condition vs Queue

Condition Queue
Programmer manually calls acquire() and release(). Queue automatically handles locking.
Programmer manually uses wait() and notify(). Queue internally handles waiting and notification.
Programmer manages shared data separately. Queue itself provides a structure for sharing data.
Suitable when manual control is required. Suitable for sharing data between Producer and Consumer Threads.

Event vs Condition vs Queue

Feature Event Condition Queue
Communication Level Simple More advanced Most advanced among these mechanisms
Main Concept Signal/Flag Wait and notification Safe data sharing
Manual Lock Management No direct manual Lock required for basic Event use Yes No
Waiting wait() wait() Handled by blocking put()/get()
Notification set() notify() / notifyAll() Handled internally
Data Sharing Not its main purpose Shared data managed separately Designed for sharing queued items

Producer and Consumer Method Comparison

Thread Queue Method Purpose
Producer q.put(item) Insert an item
Consumer q.get() Remove and return an item

Easy rule to remember:

Producer → PUT

Consumer → GET

Important Note

The biggest advantage of Queue for Inter Thread Communication is:

The queue module takes care of locking automatically.

Therefore, the programmer does not need to manually manage:

Lock
Condition
acquire()
release()
wait()
notify()

For the normal Producer–Consumer Queue workflow, put() and get() provide the required coordination.

Quick Revision Diagram

                  QUEUE
                    │
          ┌─────────┴─────────┐
          │                   │
          ▼                   ▼
      Producer             Consumer
          │                   │
          ▼                   ▼
    Generate Item         Need Item
          │                   │
          ▼                   │
      q.put(item)             │
          │                   │
          └──────► Queue ◄────┘
                     │
                     ▼
                   q.get()
                     │
                     ▼
                Consume Item


Queue Automatically Handles:

✓ Locking
✓ Waiting
✓ Notification
✓ Synchronization

Summary

  • Queue is the most enhanced mechanism for Inter Thread Communication discussed in the document.
  • Queue is especially useful for sharing data between Threads.
  • Queue internally uses synchronization mechanisms such as Condition and Lock.
  • The Queue module is imported using:
import queue
  • A Queue object is created using:
q = queue.Queue()
  • put() inserts data into the Queue.
  • get() removes and returns data from the Queue.
  • The Producer uses put().
  • The Consumer uses get().
  • put() handles locking internally.
  • get() handles locking internally.
  • If a bounded Queue is full, the Producer waits automatically when performing a blocking put().
  • If the Queue is empty, the Consumer waits automatically when performing a blocking get().
  • When an item becomes available, Queue internally coordinates the waiting Consumer.
  • The programmer does not need to manually use Lock or Condition for this Producer–Consumer communication.
  • Queue makes Inter Thread Communication easier and safer.

Important Notes

  1. Queue is the most advanced Inter Thread Communication mechanism covered in this tutorial.
  2. It is especially useful for sharing data between Producer and Consumer Threads.
  3. The document explains Queue in terms of internally using Condition and Lock.
  4. Because Queue handles synchronization, manual locking is generally not required for Queue operations.
  5. Import Queue support using import queue.
  6. Create a Queue using q = queue.Queue().
  7. The Producer inserts an item using q.put(item).
  8. The Consumer removes and receives an item using q.get().
  9. put() automatically performs the required synchronization.
  10. get() automatically performs the required synchronization.
  11. If a bounded Queue is full, a blocking put() waits until space becomes available.
  12. If a Queue is empty, a blocking get() waits until an item becomes available.
  13. Queue internally handles the required waiting and notification.
  14. The Producer does not need to manually call notify().
  15. The Consumer does not need to manually call wait().
  16. In the document program, the Producer generates values using random.randint(1,100).
  17. The Producer waits five seconds between production cycles.
  18. The Consumer waits five seconds between consumption cycles.
  19. The Producer and Consumer share the same Queue object.
  20. Queue simplifies Producer–Consumer programming compared with manually using Condition.

Python Threading Complete Summary

This section provides a quick revision of all important concepts covered in Python Multi Threading.

We have learned about:

  • Multi Tasking
  • Process Based Multi Tasking
  • Thread Based Multi Tasking
  • Applications of Multi Threading
  • Python threading module
  • Main Thread
  • Creating Threads
  • Thread methods
  • Thread information
  • Daemon Threads
  • Synchronization
  • Lock
  • RLock
  • Semaphore
  • BoundedSemaphore
  • Inter Thread Communication
  • Event
  • Condition
  • Queue

1. What is Multi Tasking?

Multi Tasking means executing several tasks simultaneously.

There are two types of Multi Tasking:

  1. Process Based Multi Tasking
  2. Thread Based Multi Tasking
                 Multi Tasking
                      │
            ┌─────────┴─────────┐
            │                   │
            ▼                   ▼
     Process Based         Thread Based
      Multi Tasking         Multi Tasking

Simple Definition:

Executing several tasks simultaneously is called Multi Tasking.

2. Process Based Multi Tasking

In Process Based Multi Tasking, every task runs as a separate process.

It is best suitable at the Operating System level.

Examples:

  • Editing a Python program
  • Listening to MP3 songs
  • Downloading a file

All these tasks execute independently.

Operating System
      │
      ├── Process 1 → Edit Python Program
      │
      ├── Process 2 → Play MP3 Song
      │
      └── Process 3 → Download File

3. Thread Based Multi Tasking

In Thread Based Multi Tasking, multiple independent parts of the same program execute simultaneously.

Each independent part is called a Thread.

Thread Based Multi Tasking is best suitable at the program level.

              One Program
                  │
        ┌─────────┼─────────┐
        │         │         │
        ▼         ▼         ▼
     Thread-1  Thread-2  Thread-3

Simple Definition:

A Thread is an independent part of the same program.

Process Based vs Thread Based Multi Tasking

Process Based Thread Based
Each task is a separate process. Each task is an independent part of the same program.
Best suitable at OS level. Best suitable at program level.
Processes execute independently. Multiple Threads belong to the same program.

4. Applications of Multi Threading

Important applications of Multi Threading include:

  • Multimedia Graphics
  • Animations
  • Video Games
  • Web Servers
  • Application Servers
Multi Threading
      │
      ├── Multimedia Graphics
      ├── Animations
      ├── Video Games
      ├── Web Servers
      └── Application Servers

5. Python Thread Module

Python provides the built-in module:

threading

to develop multi-threaded applications.

We can import it using:

import threading

or:

from threading import *

6. Main Thread

Every Python program contains one default Thread.

It is called:

MainThread

The Main Thread starts automatically when the Python program starts.

Python Program Starts
        │
        ▼
    MainThread
        │
        ▼
Execute Program

7. Ways to Create Threads

Python supports three ways to create Threads:

  1. Creating a Thread without using any class
  2. Creating a Thread by extending the Thread class
  3. Using a normal class object as Thread target
Method Concept
Method 1 Create Thread with a target function
Method 2 Extend Thread and override run()
Method 3 Use a normal class method as the Thread target

Thread Creation - Quick Example

🐍
main.py
Python 3 Runtime
Loading Editor...
Output Preview
No output captured.

Thread Creation - Execution Flow

Program Starts
      │
      ▼
MainThread
      │
      ▼
Create Thread Object
      │
      ▼
t.start()
      │
      ▼
Child Thread Starts
      │
      ├──────────► display()
      │
      ▼
Main Thread Continues

The exact execution order can vary because Thread scheduling is not predictable.

8. Important Thread Methods

The important Thread methods covered in this tutorial are:

  • start()
  • run()
  • join()
Method Purpose
start() Starts the Thread
run() Contains the work performed by the Thread
join() Makes one Thread wait until another Thread completes

join() Method

The join() method makes one Thread wait until another Thread completes its execution.

t.join()

We can also specify a waiting time:

t.join(seconds)

In this case, the calling Thread waits for the specified amount of time or until the target Thread completes.

9. Thread Name Methods

Every Thread has a name.

The tutorial covers:

  • getName()
  • setName()
  • name property

Every Thread receives a default name, which can be changed.

t.getName()

t.setName("MyThread")

t.name

10. Thread Identification Number

Every Thread has a unique identification number.

It can be accessed using:

ident

Example:

t.ident

This provides the identification number associated with the Thread.

11. active_count()

active_count() returns the number of currently active Threads.

active_count()

Conceptually:

Currently Running Threads

MainThread
Thread-1
Thread-2

active_count()
      │
      ▼
      3

12. enumerate()

enumerate() returns a list containing all currently active Thread objects.

enumerate()

It can be used when we want information about all active Threads.

13. isAlive()

isAlive() checks whether a Thread is still executing.

t.isAlive()

Conceptually:

Thread Running
     │
     ▼
   True


Thread Completed
     │
     ▼
   False

Thread Information - Quick Revision

Method / Property Purpose
getName() Get Thread name
setName() Change Thread name
name Access or modify Thread name
ident Get Thread identification number
active_count() Count active Threads
enumerate() Return active Thread objects
isAlive() Check whether Thread is executing

14. Daemon Threads

Daemon Threads run in the background and provide support to Non-Daemon Threads.

Example:

  • Garbage Collector

When the last Non-Daemon Thread terminates, the remaining Daemon Threads terminate automatically.

Main Thread
Non-Daemon Threads
       │
       ▼
All Complete
       │
       ▼
Remaining Daemon Threads
Terminate Automatically

15. Synchronization

Synchronization is used to avoid data inconsistency when multiple Threads access shared resources.

The synchronization techniques covered are:

  • Lock
  • RLock
  • Semaphore
Synchronization
      │
      ├── Lock
      ├── RLock
      └── Semaphore

Simple Definition:

Synchronization controls concurrent access to shared resources so that data inconsistency can be avoided.

Why Synchronization?

Without proper synchronization, multiple Threads may access the same shared resource simultaneously.

Thread-1 ─────┐
              │
              ▼
        Shared Resource
              ▲
              │
Thread-2 ─────┘

Possible Problem
      │
      ▼
Data Inconsistency

Synchronization controls access to the shared resource.

16. Lock

A Lock allows only one Thread at a time to enter the protected section.

Important methods:

acquire()
release()

Basic pattern:

l.acquire()

# Critical Section

l.release()
Thread-1
   │
   ▼
Acquire Lock
   │
   ▼
Critical Section
   │
   ▼
Release Lock
   │
   ▼
Next Waiting Thread

17. RLock

RLock means Reentrant Lock.

The same Thread can acquire an RLock multiple times.

This is useful for:

  • Recursive functions
  • Nested resource access
Same Thread
    │
    ├── acquire()
    ├── acquire()
    ├── acquire()
    │
    ▼
Allowed with RLock

The Thread must release the RLock appropriately for its acquisitions.

Lock vs RLock

Lock RLock
A Thread cannot safely acquire the same Lock again while already holding it. The owning Thread can acquire the same RLock multiple times.
Repeated acquisition by the same Thread can block it. Supports reentrant acquisition.
Suitable for basic synchronization. Useful for recursive and nested locking.

18. Semaphore

A Semaphore allows a fixed number of Threads to access a protected section simultaneously.

Example:

s = Semaphore(3)

Here, up to three Threads can acquire the Semaphore at the same time.

Semaphore(3)

Thread-1 ──► Allowed
Thread-2 ──► Allowed
Thread-3 ──► Allowed
Thread-4 ──► Wait
Thread-5 ──► Wait

Lock vs Semaphore

Lock Semaphore
Only one Thread can acquire it at a time. A fixed number of Threads can acquire it at the same time.
Used for exclusive access. Used for limited concurrent access.

19. BoundedSemaphore

BoundedSemaphore is similar to Semaphore.

The important difference is that it prevents the Semaphore counter from being released beyond its initial bound.

The tutorial demonstrates this with extra release() calls.

s = BoundedSemaphore(2)

s.acquire()
s.acquire()

s.release()
s.release()

# Extra release
s.release()

This results in:

ValueError: Semaphore released too many times

This can help detect programming mistakes involving unmatched release() operations.

20. Inter Thread Communication

Sometimes multiple Threads need to communicate with each other.

This concept is called Inter Thread Communication.

Python mechanisms covered in the tutorial are:

  • Event
  • Condition
  • Queue
        Inter Thread Communication
                   │
         ┌─────────┼─────────┐
         │         │         │
         ▼         ▼         ▼
       Event   Condition    Queue

Producer-Consumer Concept

A common example of Inter Thread Communication is the Producer–Consumer model.

Producer Thread
      │
      ▼
Produce Item
      │
      ▼
Communicate
      │
      ▼
Consumer Thread
      │
      ▼
Consume Item

The Producer creates new items, while the Consumer waits for and consumes those items.

21. Event

An Event provides a simple mechanism for communication between Threads.

Important methods covered in the tutorial:

  • set()
  • clear()
  • isSet()
  • wait()
Method Purpose
set() Sets the internal flag to True
clear() Sets the internal flag to False
isSet() Checks whether the Event is set
wait() Waits until the Event becomes set

Event - Quick Flow

Consumer
   │
   ▼
event.wait()
   │
   ▼
Waiting
   │
   │
   │ Producer
   │
   ▼
event.set()
   │
   ▼
Consumer Continues

22. Condition

Condition provides a more advanced communication mechanism.

Important methods:

  • acquire()
  • release()
  • wait()
  • notify()
  • notifyAll()
Method Purpose
acquire() Acquire the associated Lock
release() Release the associated Lock
wait() Wait for notification
notify() Notify one waiting Thread
notifyAll() Notify all waiting Threads

Condition - Producer Consumer Flow

Consumer
   │
   ▼
acquire()
   │
   ▼
wait()
   │
   ▼
Waiting
   ▲
   │
   │ notify()
   │
Producer
   │
   ▼
acquire()
   │
   ▼
Produce Item
   │
   ▼
notify()
   │
   ▼
release()

The Consumer generally waits, while the Producer performs the update and sends the notification.

23. Queue

Queue provides an easy and powerful mechanism for sharing data between Threads.

The important methods are:

  • put()
  • get()
Producer
   │
   ▼
q.put(item)
   │
   ▼
 Queue
   │
   ▼
q.get()
   │
   ▼
Consumer

Queue automatically handles:

  • Locking
  • Waiting
  • Notification

Queue Behaviour

If a bounded Queue is full, a blocking Producer waits until space becomes available.

If the Queue is empty, a blocking Consumer waits until an item becomes available.

Queue Full
    │
    ▼
Producer Waits


Queue Empty
    │
    ▼
Consumer Waits

This synchronization is handled automatically by Queue.

Event vs Condition vs Queue

Event Condition Queue
Simple communication mechanism More advanced communication mechanism Most enhanced mechanism covered in the tutorial
Works using an internal flag Uses waiting and notification with locking Designed for synchronized data sharing
set(), clear(), wait() wait(), notify(), etc. put(), get()
Useful for signaling Useful when manual control is required Very convenient for Producer–Consumer communication

Complete Threading Concept Flow

Python Multi Threading
        │
        ├── Multi Tasking
        │      ├── Process Based
        │      └── Thread Based
        │
        ├── threading Module
        │
        ├── MainThread
        │
        ├── Thread Creation
        │      ├── Target Function
        │      ├── Extend Thread
        │      └── Normal Class Method
        │
        ├── Thread Operations
        │      ├── start()
        │      ├── run()
        │      └── join()
        │
        ├── Thread Information
        │      ├── name
        │      ├── ident
        │      ├── active_count()
        │      ├── enumerate()
        │      └── isAlive()
        │
        ├── Daemon Threads
        │
        ├── Synchronization
        │      ├── Lock
        │      ├── RLock
        │      ├── Semaphore
        │      └── BoundedSemaphore
        │
        └── Inter Thread Communication
               ├── Event
               ├── Condition
               └── Queue

Complete Revision Table

Topic Important Points
Multi Tasking Process Based and Thread Based
Thread Module threading
Main Thread MainThread
Thread Creation Three ways
Main Methods start(), run(), join()
Thread Information getName(), setName(), ident, active_count(), enumerate(), isAlive()
Daemon Thread Background support Thread
Synchronization Lock, RLock, Semaphore
Lock One Thread at a time
RLock Same owning Thread can acquire multiple times
Semaphore Fixed number of Threads can access simultaneously
BoundedSemaphore Prevents releases beyond the initial bound
Inter Thread Communication Event, Condition, Queue
Event Methods set(), clear(), isSet(), wait()
Condition Methods acquire(), release(), wait(), notify(), notifyAll()
Queue Methods put(), get()

Summary

  • Multi Tasking means executing several tasks simultaneously.
  • Multi Tasking can be Process Based or Thread Based.
  • A Thread is an independent part of the same program.
  • Python provides the threading module for Multi Threading.
  • Every Python program starts with a default MainThread.
  • Python supports three approaches to creating Threads covered in this tutorial.
  • start() starts a Thread.
  • run() contains the Thread's job when working with the Thread class.
  • join() can make one Thread wait for another.
  • Thread information can be obtained using names, ident, active_count(), enumerate(), and Thread-status methods.
  • Daemon Threads provide background support to Non-Daemon Threads.
  • Synchronization is used to protect shared resources from data inconsistency.
  • Lock permits one Thread at a time.
  • RLock supports repeated acquisition by the owning Thread.
  • Semaphore permits a fixed number of Threads simultaneously.
  • BoundedSemaphore can detect excessive releases.
  • Event, Condition, and Queue are used for Inter Thread Communication.
  • Event provides simple signaling.
  • Condition provides controlled waiting and notification.
  • Queue simplifies synchronized data sharing between Producer and Consumer Threads.

Important Notes

  1. The threading module is used to develop multi-threaded Python applications.
  2. Every Python program contains a Main Thread.
  3. The execution order of multiple Threads is generally not predictable.
  4. start() should be used to start a Thread.
  5. join() is used when one Thread must wait for another Thread.
  6. ident provides the Thread identification number.
  7. active_count() returns the number of active Threads.
  8. enumerate() returns active Thread objects.
  9. Daemon Threads work in the background to support Non-Daemon Threads.
  10. Synchronization is important when multiple Threads access shared resources.
  11. Lock provides exclusive access to one Thread at a time.
  12. RLock is useful when the owning Thread needs to acquire the same lock repeatedly.
  13. Semaphore allows a fixed number of Threads to enter simultaneously.
  14. BoundedSemaphore helps identify incorrect extra release() operations.
  15. Event is the simplest Inter Thread Communication mechanism covered in this tutorial.
  16. Condition provides more control over waiting and notification.
  17. Queue provides automatic synchronization for Producer–Consumer data sharing.
  18. The Producer generally uses put() with Queue.
  19. The Consumer generally uses get() with Queue.
  20. Queue automatically handles locking, waiting, and notification.

🧠 Test Your Knowledge

257 Questions

Progress: 0 / 257
Keep Going!Python - Database