Multithreading

By the Syncfusion DevTutorials team · Last updated

Threads, processes, the main thread and child threads, the threading module, locks and events, and why the browser cannot start one.

A school office is making report cards. One clerk does everything: fetch a file, type the marks, print, fetch the next file.

Most of that clerk's day is spent waiting — for the cabinet, for the printer. Hire a second clerk and the waiting overlaps: one types while the other waits at the printer. Nobody types faster. The queue just moves.

That is what threads are for.

Here is that day measured — two fetches in a row, and the time they spend not working:

Two tasks, one after the other

import time

def fetch(name):
    time.sleep(0.05)
    return f"{name}'s file"

started = time.perf_counter()

print(fetch("Asha"))
print(fetch("Ravi"))

print(f"Took {(time.perf_counter() - started) * 1000:.0f} ms, and most of it was waiting")

A thread is a separate line of execution inside one program. Threads help when a program spends its time waiting — for a file, a network, a printer. They do not make calculations faster.

This page is mostly code text, not runnable cells, and that is deliberate. Python in this page runs inside your browser tab, and a browser tab cannot start an operating-system thread. Every example that actually starts a thread is shown as plain code to read and run on your own computer. The cell below shows exactly what the browser says, so you can see why rather than take my word for it.

Why the examples below are not runnable here

import threading

def work():
    print("working")

worker = threading.Thread(target=work)
worker.start()

Everything else about threading does work here — naming threads, locks, events, the main thread — and those cells are runnable. Only start() is refused.

What You Will Learn

In this lesson, you will learn:

  • What a thread is, and how it differs from a process
  • Why threads help with waiting and not with arithmetic
  • The main thread, child threads and the stages a thread passes through
  • The threading module and the five pieces you will actually use
  • Why two threads sharing one number can get the wrong answer, and what a lock does about it

By the end of this lesson, you will be able to tell a thread from a process, start a thread and wait for it, protect shared data with a lock, and know exactly when threads help and when they do not.

Introduction to Multithreading

What Is Multithreading?

Multithreading means a single program doing more than one thing at a time by running several threads inside itself. Each thread has its own place in the code and its own local variables, but they all share the program's memory.

Why Multithreading Is Used

Because most programs spend most of their time waiting. Reading a file, waiting for a reply over the network, waiting for a person to type — during all of that, the processor is idle. A second thread can use that idle time.

Multithreading against Sequential Execution

Here are the two ways of working side by side:

SequentialMultithreaded
TasksOne finishes before the next startsSeveral are in progress at once
Total time for three one-second waitsAbout three secondsAbout one second
Total time for three heavy calculationsAbout the same either wayAbout the same either way
ComplexityLowShared data must be protected
DebuggingWhat you see is what ranThe order changes between runs

Where the time actually goes

import time

def wait_for_printer():
    time.sleep(0.03)

started = time.perf_counter()
for _ in range(3):
    wait_for_printer()
waiting = (time.perf_counter() - started) * 1000

started = time.perf_counter()
total = sum(n * n for n in range(200000))
working = (time.perf_counter() - started) * 1000

print(f"Three waits:      {waiting:.0f} ms — threads would overlap this")
print(f"One calculation:  {working:.0f} ms — threads would not help here")

The first number is time spent doing nothing. That is the kind threads reclaim.

What Is a Thread?

Definition of a Thread

A thread is one sequence of instructions being carried out. Every program has at least one. A program with several threads has several places in the code being worked on at once.

Process against Thread

A process is a running program with its own memory. A thread lives inside a process and shares that memory with every other thread in it.

Here is the difference that matters:

ProcessThread
MemoryIts own, privateShared with the other threads
Cost to startHighLow
If one crashesThe others are unaffectedThe whole program can go down
Sharing dataNeeds deliberate machineryAutomatic — which is the danger
Python modulemultiprocessingthreading

That "shared memory" row is the whole reason the rest of this page exists. Two threads changing the same variable is easy to write and easy to get wrong.

Several Threads in a Program

Here is a program with three threads in it, drawn out:

A program (one process)
├── MainThread        running your file, top to bottom
├── Thread-1          fetching Asha's file
└── Thread-2          printing Ravi's card

All three share the same variables, the same open files and the same memory. Each has its own call stack and its own local names.

Why Use Multithreading?

Better Responsiveness

An application that saves a large file on its main thread freezes until the save finishes — no buttons, no scrolling. Move the save to a second thread and the window keeps working.

Performing Several Tasks

Downloading twenty report files one after another means twenty waits in a row. Twenty threads means roughly one wait, because the waiting overlaps.

Background Operations

Work nobody is waiting to see: writing a log, checking for updates, saving a draft every thirty seconds. It happens on a thread while the program gets on with its job.

Real-World Examples

Here are places you use every day that do the same thing:

WhereWhat the background thread does
A text editorAuto-saves while you keep typing
A browserLoads images while the page is already readable
A music playerReads ahead so the sound never breaks
A web serverHandles each visitor on its own thread
A school appUploads the day's marks while the teacher moves on

Threads do not make Python arithmetic faster. Python has a global interpreter lock, which allows only one thread to run Python code at a time. Threads help when the waiting happens outside Python — in a file read, a network call, or time.sleep. For heavy calculation, multiprocessing is the tool.

Main Thread and Child Threads

The Main Thread

Every Python program starts with one thread already running: the main thread. It is the one running your file from top to bottom. You never create it.

Here it is, looked at rather than started:

The thread you are already in

import threading

current = threading.current_thread()

print("Name:      ", current.name)
print("Alive:     ", current.is_alive())
print("Is main:   ", current is threading.main_thread())
print("Thread count:", threading.active_count())

That cell runs here, because it only looks at threads.

Child Threads

A child thread is one your program starts. You give it a function to run and the arguments to pass.

import threading

def fetch(name):
    print("fetching", name)

worker = threading.Thread(target=fetch, args=("Asha",))
worker.start()
worker.join()
print("done")
  • target is the function the thread will run. Note there are no brackets after it — you are handing over the function, not calling it.
  • args is a tuple of arguments for that function.
  • start() begins it. Calling run() instead would just run the function on the current thread, which is a common and silent mistake.
  • join() waits for it to finish.

Thread Lifecycle Overview

Here are the stages a thread passes through, from start to finish:

StageWhat it meansHow you get there
NewThe object exists; nothing is runningThread(target=...)
RunnableStarted, and waiting for its turnstart()
RunningCarrying out its instructionsThe operating system decides
BlockedWaiting for a file, a lock or a sleeptime.sleep(), a lock
FinishedThe function returnedAutomatic

A thread cannot be restarted. Once it has finished, calling start() again raises RuntimeError.

Daemon Threads

A daemon thread is one the program will not wait for. When the main thread ends, daemon threads are stopped wherever they are.

logger = threading.Thread(target=write_log, daemon=True)
logger.start()

Use it for genuine background chores. Never for anything that must finish — a daemon writing a file can be cut off mid-sentence.

The threading Module

What Is the threading Module?

threading is part of the standard library. Nothing to install: import threading and it is there.

Importing it works fine — it is starting a thread that does not

import threading

print(threading.__name__)
print("Main thread:", threading.main_thread().name)
print("Running now:", threading.active_count())

Common Threading Components

Here are the pieces you will actually meet:

PieceWhat it is for
ThreadOne thread. target, args, daemon, name
start()Begins the thread
join()Waits for a thread to finish
LockLets one thread at a time into a section of code
RLockA lock the same thread may take again
EventOne thread waits for a signal from another
SemaphoreAllows a fixed number of threads at once
current_thread()The thread running this line
active_count()How many threads are alive

The Shared-Data Problem

Two threads adding to the same number can lose some of the additions, because total += 1 is three steps — read, add, store — and a thread can be interrupted between them.

import threading

total = 0

def add_many():
    global total
    for _ in range(100000):
        total += 1

threads = [threading.Thread(target=add_many) for _ in range(2)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(total)       # expected 200000 — and sometimes it is not

Locks

A lock is a token only one thread can hold. A thread that wants the token waits until the holder gives it back.

A lock, used by one thread

import threading

lock = threading.Lock()
total = 0

with lock:
    total += 1

print("Inside the lock, total is", total)
print("Lock held now:", lock.locked())

That cell runs here because one thread taking a lock and giving it back needs no second thread. With two threads it would look like this:

lock = threading.Lock()
total = 0

def add_many():
    global total
    for _ in range(100000):
        with lock:
            total += 1

with lock: takes the lock and gives it back even if the body raises — the same reason with is used for files.

Events

An event is a flag one thread sets and another waits for.

An event, set and checked

import threading

marks_ready = threading.Event()

print("Ready at the start:", marks_ready.is_set())
marks_ready.set()
print("Ready after set:   ", marks_ready.is_set())
marks_ready.clear()
print("Ready after clear: ", marks_ready.is_set())

In a real program the other thread calls marks_ready.wait(), which blocks until something calls set().

Most programs that need threads are better served by concurrent.futures.ThreadPoolExecutor, which starts a pool, hands out the work, collects the results and joins everything for you. Learn Thread to understand what is happening; reach for the pool when you write it for real.

from concurrent.futures import ThreadPoolExecutor

def fetch(name):
    return f"{name}'s file"

with ThreadPoolExecutor(max_workers=3) as pool:
    for result in pool.map(fetch, ["Asha", "Ravi", "Meera"]):
        print(result)

Common mistakes

Before moving on, here are some of the mistakes beginners make with these topics.

Each row is one mistake. The middle column is what you will actually see on screen.

MistakeWhat you seeWhy it happensFix
Thread(target=fetch())The function runs immediately, on the main threadThe brackets called ittarget=fetch, args=(...)
Calling worker.run()It works, but nothing is concurrentrun() runs it hereCall start()
Forgetting join()The program ends before the results arriveNothing waitedjoin() every thread
args=("Asha")TypeError about argumentsThat is a string, not a tupleargs=("Asha",)
Starting a finished thread againRuntimeError: threads can only be started onceA thread is single-useMake a new Thread
Sharing a counter with no lockA total that is wrong, sometimestotal += 1 is three stepswith lock:
Expecting threads to speed up arithmeticThe same time, or slowerThe global interpreter lockUse multiprocessing
A daemon thread writing a fileA half-written fileDaemons are stopped at exitDo not make it a daemon
Relying on the order of outputIt changes between runsThe operating system choosesCollect results, order them after
Testing threads in this pageRuntimeError: can't start new threadA browser tab cannot start oneRun the code on your computer

What to Remember

  • A thread is a separate line of execution inside one program; every program starts with one, the main thread.
  • A process has its own memory; threads share theirs. That sharing is the benefit and the danger.
  • Threads help with waiting — files, networks, sleeping. They do not speed up Python arithmetic, because of the global interpreter lock.
  • Thread(target=func, args=(x,)) — no brackets after the function name, and a one-item args needs its trailing comma.
  • start() makes a new thread; run() just runs the function where you are.
  • join() waits for a thread. Without it, your program can end before the work does.
  • A thread can be started once. Finished means finished.
  • A daemon thread is abandoned when the program ends — never give one work that must complete.
  • total += 1 is three steps, so two threads can lose an addition. with lock: makes it one step.
  • An Event is a flag one thread sets and another waits for.
  • ThreadPoolExecutor is what you should actually reach for once you understand Thread.
  • The browser cannot start a thread, which is why most of this page is code to run on your own computer.

Next: Python References