In this tutorial, you’ll learn how to use Python’s built-in threading module to explore multithreading capabilities in Python.

Starting with the basics of processes and threads, you’ll learn how multithreading works in Python—while understanding the concepts of concurrency and parallelism. You’ll then learn how to start and run one or more threads in Python using the built-in threading module.

Let’s get started.

Processes vs. Threads: What Are the Differences?

What Is a Process?

A process is any instance of a program that needs to run.

It can be anything – a Python script or a web browser such as Chrome to a video-conferencing application. If you launch the Task Manager on your machine and navigate to Performance –> CPU, you’ll be able to see the processes and threads that are currently running on your CPU cores.

<img alt="cpu-proc-threads" data- data-src="https://kirelos.com/wp-content/uploads/2022/10/echo/3-3-1500×844.png" data- height="422" src="data:image/svg xml,” width=”750″>

Understanding Processes and Threads

Internally, a process has a dedicated memory that stores the code and data corresponding to the process.

A process consists of one or more threads. A thread is the smallest sequence of instructions that the operating system can execute, and it represents the flow of execution.

Each thread has its own stack and registers but not a dedicated memory. All the threads associated with a process can access the data. Therefore, data and memory are shared by all the threads of a process.

<img alt="process-and-threads" data- data-src="https://kirelos.com/wp-content/uploads/2022/10/echo/1-3-1500×844.png" data- height="422" src="data:image/svg xml,” width=”750″>

In a CPU with N cores, N processes can execute in parallel at the same instance of time. However, two threads of the same process can never execute in parallel- but can execute concurrently. We’ll address the concept of concurrency vs. parallelism in the next section.

Based on what we’ve learned so far, let’s summarize the differences between a process and a thread.

Feature Process Thread
Memory Dedicated memory Shared memory
Mode of execution Parallel, concurrent Concurrent; but not parallel
Execution handled by Operating System CPython Interpreter

Multithreading in Python

In Python, the Global Interpreter Lock (GIL) ensures that only one thread can acquire the lock and run at any point in time. All threads should acquire this lock to run. This ensures that only a single thread can be in execution—at any given point in time—and avoids simultaneous multithreading.

For example, consider two threads, t1 and t2, of the same process. Because threads share the same data when t1 is reading a particular value k, t2 may modify the same value k. This can lead to deadlocks and undesirable results. But only one of the threads can acquire the lock and run at any instance. Therefore, GIL also ensures thread safety.

So how do we achieve multithreading capabilities in Python? To understand this, let’s discuss the concepts of concurrency and parallelism.

Concurrency vs. Parallelism: An Overview

Consider a CPU with more than one core. In the illustration below, the CPU has four cores. This means that we can have four different operations running in parallel at any given instant.

If there are four processes, then each of the processes can run independently and simultaneously on each of the four cores. Let’s assume that each process has two threads.

<img alt="multicore-parallelism" data- data-src="https://kirelos.com/wp-content/uploads/2022/10/echo/2-3-1500×844.png" data- height="422" src="data:image/svg xml,” width=”750″>

To understand how threading works, let us switch from multicore to single-core processor architecture. As mentioned, only a single thread can be active at a particular execution instance; but the processor core can switch between the threads. 

<img alt="code" data- data-src="https://kirelos.com/wp-content/uploads/2022/10/echo/code-1500×844.png" data- height="422" src="data:image/svg xml,” width=”750″>

For example, I/O-bound threads often wait for I/O operations: reading in user input, database reads, and file operations. During this waiting time, it can release the lock so that the other thread can run. The waiting time can also be a simple operation such as sleeping for n seconds.

In summary: During wait operations, the thread releases the lock, enabling the processor core to switch to another thread. The earlier thread resumes execution after the waiting period is complete. This process, where the processor core switches between the threads concurrently, facilitates multithreading. ✅

If you want to implement process-level parallelism in your application, consider using multiprocessing instead.

Python Threading Module: First Steps

Python ships with a threading module that you can import into the Python script.

import threading

To create a thread object in Python, you can use the Thread constructor: threading.Thread(...). This is the generic syntax that suffices for most threading implementations:

threading.Thread(target=...,args=...)

Here,

  • target is the keyword argument denoting a Python callable
  • args is the tuple of arguments that the target takes in.

You’ll need Python 3.x to run the code examples in this tutorial. Download the code and follow along.

How to Define and Run Threads in Python

Let’s define a thread that runs a target function.

The target function is some_func.

import threading
import time

def some_func():
    print("Running some_func...")
    time.sleep(2)
    print("Finished running some_func.")

thread1 = threading.Thread(target=some_func)
thread1.start()
print(threading.active_count())

Let’s parse what the above code snippet does:

  • It imports the threading and the time modules.
  • The function some_func has descriptive print() statements and includes a sleep operation for two seconds: time.sleep(n) causes the function to sleep for n seconds.
  • Next, we define a thread thread_1 with the target as some_func. threading.Thread(target=...) creates a thread object.
  • Note: Specify the name of the function and not a function call; use some_func and not some_func().
  • Creating a thread object does not start a thread; calling the start() method on the thread object does.
  • To get the number of active threads, we use the active_count() function.

The Python script is running on the main thread, and we are creating another thread (thread1) to run the function some_func so the active thread count is two, as seen in the output:

# Output
Running some_func...
2
Finished running some_func.

If we take a closer look at the output, we see that upon starting thread1, the first print statement runs. But during the sleep operation, the processor switches to the main thread and prints out the number of active threads—without waiting for thread1 to finish executing.

<img alt="thread1-ex" data-src="https://kirelos.com/wp-content/uploads/2022/10/echo/thread1-ex.png" height="350" src="data:image/svg xml,” width=”750″>

Waiting for Threads to Finish Execution

If you want thread1 to finish the execution, you can call the join() method on it after starting the thread. Doing so will wait for thread1 to finish execution without switching to the main thread.

import threading
import time

def some_func():
    print("Running some_func...")
    time.sleep(2)
    print("Finished running some_func.")

thread1 = threading.Thread(target=some_func)
thread1.start()
thread1.join()
print(threading.active_count())

Now, thread1 has finished executing before we print out the active thread count. So only the main thread is running, which means the active thread count is one. ✅

# Output
Running some_func...
Finished running some_func.
1

How to Run Multiple Threads in Python

Next, let’s create two threads to run two different functions. 

Here, count_down is a function that takes in a number as the argument and counts down from that number to zero.

def count_down(n):
    for i in range(n,-1,-1):
        print(i)

We define count_up, another Python function that counts from zero up to a given number.

def count_up(n):
    for i in range(n 1):
        print(i)

📑 When using the range() function with the syntax range(start, stop, step), the end point stop is excluded by default.

– To count down from a specific number to zero, you can use a negative step value of -1 and set the stop value to -1 so that zero is included.

– Similarly, to count up to n, you have to set the stop value to n 1. Because the default values of start and step are 0 and 1, respectively, you may use range(n 1) to get the sequence 0 through n.

Next, we define two threads, thread1 and thread2 to run the functions count_down and count_up, respectively. We add print statements and sleep operations for both the functions.

When creating the thread objects, notice that the arguments to the target function should be specified as a tuple—to the args parameter. As both the functions (count_down and count_up) take in one argument. You’ll have to insert a comma explicitly after the value. This ensures the argument is still passed in as a tuple, as the subsequent elements are inferred as None.

import threading
import time

def count_down(n):
    for i in range(n,-1,-1):
        print("Running thread1....")
        print(i)
        time.sleep(1)


def count_up(n):
    for i in range(n 1):
        print("Running thread2...")
        print(i)
        time.sleep(1)

thread1 = threading.Thread(target=count_down,args=(10,))
thread2 = threading.Thread(target=count_up,args=(5,))
thread1.start()
thread2.start()

In the output:

  • The function count_up runs on thread2 and counts up to 5 starting at 0. 
  • The count_down function runs on thread1 counts down from 10 to 0.
# Output
Running thread1....
10
Running thread2...
0
Running thread1....
9
Running thread2...
1
Running thread1....
8
Running thread2...
2
Running thread1....
7
Running thread2...
3
Running thread1....
6
Running thread2...
4
Running thread1....
5
Running thread2...
5
Running thread1....
4
Running thread1....
3
Running thread1....
2
Running thread1....
1
Running thread1....
0

You can see that thread1 and thread2 execute alternatively, as both of them involve a wait operation (sleep). Once the count_up function has finished counting up to 5, thread2 is no longer active. So we get the output corresponding to only thread1.

Summing Up

In this tutorial, you’ve learned how to use Python’s built-in threading module to implement multithreading. Here’s a summary of the key takeaways:

  • The Thread constructor can be used to create a thread object. Using threading.Thread(target=,args=()) creates a thread that runs the target callable with arguments specified in args.
  • The Python program runs on a main thread, so the thread objects you create are additional threads. You can call active_count() function returns the number of active threads at any instance.
  • You can start a thread using the start() method on the thread object and wait until it finishes execution using the join() method.

You can code additional examples by tweaking the waiting times, trying for a different I/O operation, and more. Be sure to implement multithreading in your upcoming Python projects. Happy coding!🎉