Nearby lessons
137 of 159Python - Creating Threads
- Create a thread without extending the Thread class
- Pass a target method to Thread(target=obj.display)
- Understand the three ways of creating threads
- Compare sequential and threaded execution time
- Use args=(numbers,) to pass data to a thread function
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
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
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
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:
t1to completet2to 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
- A thread can be created using a normal class without extending the
Threadclass. - In this approach, create an object of the normal class and pass its method as the Thread target.
Thread(target=obj.display)executes the object'sdisplay()method in a separate thread afterstart()is called.- The normal class itself does not need to inherit from
Thread. - When the Main Thread and Child Thread execute concurrently, the exact output order cannot be predicted.
- In the non-threaded program,
doubles()finishes beforesquares()starts. - Therefore, the non-threaded version executes the two functions sequentially.
- In the multithreaded program, one Child Thread executes
doubles()and another executessquares(). args=(numbers,)passes thenumberslist as an argument to the target function.- The comma in
(numbers,)makes it a one-element tuple. start()starts each Child Thread.join()makes the Main Thread wait until the Child Thread completes.- The Main Thread waits for both Child Threads before displaying the total execution time.
- In this example, sequential execution takes approximately 12 seconds because each function waits about 6 seconds.
- With Multi Threading, the waiting periods overlap, so the example takes approximately 6 seconds.
- The exact timing can vary slightly depending on the system and execution overhead.
- Thread(target=obj.display) creates a thread from a bound method
- t.start() begins the child thread execution
- args=(numbers,) passes a tuple of arguments to the target
- With multi threading, doubles() and squares() run concurrently
- The threaded version finishes faster in I/O-bound examples