Nearby lessons

137 of 159

Python - Creating Threads

📌 What You Will Learn
  • 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

🐍Code Cell
1from threading import *
2 
3class Test:
4 
5 def display(self):
6 for i in range(10):
7 print("Child Thread-2")
8 
9obj = Test()
10 
11t = Thread(target=obj.display)
12t.start()
13 
14for i in range(10):
15 print("Main Thread-2")
Output
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

🐍Code Cell
1from threading import *
2import time
3 
4def doubles(numbers):
5 for n in numbers:
6 time.sleep(1)
7 print("Double:", 2 * n)
8 
9def squares(numbers):
10 for n in numbers:
11 time.sleep(1)
12 print("Square:", n * n)
13 
14numbers = [1, 2, 3, 4, 5, 6]
15 
16begintime = time.time()
17 
18doubles(numbers)
19squares(numbers)
20 
21print("The total time taken:",
22 time.time() - begintime)
Output
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

🐍Code Cell
1from threading import *
2import time
3 
4def doubles(numbers):
5 for n in numbers:
6 time.sleep(1)
7 print("Double:", 2 * n)
8 
9def squares(numbers):
10 for n in numbers:
11 time.sleep(1)
12 print("Square:", n * n)
13 
14numbers = [1, 2, 3, 4, 5, 6]
15 
16begintime = time.time()
17 
18t1 = Thread(target=doubles, args=(numbers,))
19t2 = Thread(target=squares, args=(numbers,))
20 
21t1.start()
22t2.start()
23 
24t1.join()
25t2.join()
26 
27print("The total time taken:",
28 time.time() - begintime)
Output
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.
📝 Key Takeaways
  • 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

🧠 Test Your Knowledge

16 Questions
Progress: 0 / 16