Understanding Multithreading in Python with Examples

Multithreading in Python is a powerful way to make your programs more efficient by allowing multiple threads to execute concurrently. This can help improve the performance of your applications, especially those that perform a lot of I/O operations or have tasks that can run independently. In this blog, we will explore what multithreading is, why it's useful, and how to implement it in Python with practical examples.

What is Multithreading?

Multithreading is a technique where multiple threads are created within a process to execute tasks concurrently. A thread is the smallest unit of a process that can be scheduled by the operating system. Each thread runs independently, but they share the same memory space, which allows them to communicate and share data easily.

Benefits of Multithreading

  1. Improved Performance: Multithreading can significantly improve the performance of I/O-bound applications, such as web servers or file I/O operations, by allowing multiple tasks to run concurrently.

  2. Resource Sharing: Since threads within the same process share the same memory space, they can easily share data without the need for complex inter-process communication mechanisms.

  3. Responsiveness: Multithreading can make your applications more responsive, especially those with user interfaces, by allowing background tasks to run without freezing the main application.

Multithreading in Python

Python provides a built-in module called threading to handle multithreading. Let's dive into some examples to understand how it works.

Example 1: Basic Multithreading

In this example, we will create two threads that print numbers from 1 to 5.

import threading

def print_numbers():
    for i in range(1, 6):
        print(f'Number: {i}')

# Creating threads
thread1 = threading.Thread(target=print_numbers)
thread2 = threading.Thread(target=print_numbers)

# Starting threads
thread1.start()
thread2.start()

# Waiting for both threads to complete
thread1.join()
thread2.join()

print('Both threads have finished execution.')

In this example:

  • We define a function print_numbers that prints numbers from 1 to 5.

  • We create two threads thread1 and thread2, each targeting the print_numbers function.

  • We start both threads using the start() method.

  • We wait for both threads to complete using the join() method.

Example 2: Multithreading with Arguments

You can also pass arguments to the target function of a thread.

import threading

def print_numbers(name):
    for i in range(1, 6):
        print(f'{name} - Number: {i}')

# Creating threads with arguments
thread1 = threading.Thread(target=print_numbers, args=('Thread1',))
thread2 = threading.Thread(target=print_numbers, args=('Thread2',))

# Starting threads
thread1.start()
thread2.start()

# Waiting for both threads to complete
thread1.join()
thread2.join()

print('Both threads have finished execution.')

In this example, we pass a name argument to the print_numbers function to identify which thread is printing the numbers.

Example 3: Synchronizing Threads

Sometimes, you need to synchronize threads to prevent race conditions when accessing shared resources. You can use a Lock object for this purpose.

import threading

counter = 0
lock = threading.Lock()

def increment_counter():
    global counter
    for _ in range(1000):
        lock.acquire()
        counter += 1
        lock.release()

# Creating threads
thread1 = threading.Thread(target=increment_counter)
thread2 = threading.Thread(target=increment_counter)

# Starting threads
thread1.start()
thread2.start()

# Waiting for both threads to complete
thread1.join()
thread2.join()

print(f'Final counter value: {counter}')

In this example:

  • We use a global variable counter and a Lock object lock.

  • The increment_counter function increments the counter 1000 times, but only after acquiring the lock, ensuring only one thread modifies counter at a time.

Example 4: Thread Pooling

Thread pooling is a technique where a pool of threads is created, and tasks are assigned to them. This can be more efficient than creating and destroying threads frequently.

import concurrent.futures

def task(n):
    print(f'Task {n} is running')
    return n * 2

with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
    futures = [executor.submit(task, i) for i in range(5)]
    results = [future.result() for future in concurrent.futures.as_completed(futures)]

print('Results:', results)

In this example:

  • We use the ThreadPoolExecutor from the concurrent.futures module to create a pool of threads.

  • We submit tasks to the thread pool using the submit method.

  • We collect the results as the tasks complete using the as_completed method.


Conclusion
Multithreading in Python can significantly enhance the performance and responsiveness of your applications. By using the threading module and synchronization techniques like locks, you can efficiently manage concurrent tasks. Whether you need to perform I/O operations, handle user interfaces, or run background tasks, multithreading is a valuable tool in your Python programming arsenal.

Experiment with the examples provided and explore more advanced concepts like thread synchronization, thread pooling, and concurrent programming to become proficient in multithreading in Python. Happy coding!

Support us by subscribing to the blog, thank you!

31
Subscribe to my newsletter

Read articles from ByteScrum Technologies directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

ByteScrum Technologies
ByteScrum Technologies

Our company comprises seasoned professionals, each an expert in their field. Customer satisfaction is our top priority, exceeding clients' needs. We ensure competitive pricing and quality in web and mobile development without compromise.