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
threadingmodule 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:
| Sequential | Multithreaded | |
|---|---|---|
| Tasks | One finishes before the next starts | Several are in progress at once |
| Total time for three one-second waits | About three seconds | About one second |
| Total time for three heavy calculations | About the same either way | About the same either way |
| Complexity | Low | Shared data must be protected |
| Debugging | What you see is what ran | The 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:
| Process | Thread | |
|---|---|---|
| Memory | Its own, private | Shared with the other threads |
| Cost to start | High | Low |
| If one crashes | The others are unaffected | The whole program can go down |
| Sharing data | Needs deliberate machinery | Automatic — which is the danger |
| Python module | multiprocessing | threading |
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:
| Where | What the background thread does |
|---|---|
| A text editor | Auto-saves while you keep typing |
| A browser | Loads images while the page is already readable |
| A music player | Reads ahead so the sound never breaks |
| A web server | Handles each visitor on its own thread |
| A school app | Uploads 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")
targetis the function the thread will run. Note there are no brackets after it — you are handing over the function, not calling it.argsis a tuple of arguments for that function.start()begins it. Callingrun()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:
| Stage | What it means | How you get there |
|---|---|---|
| New | The object exists; nothing is running | Thread(target=...) |
| Runnable | Started, and waiting for its turn | start() |
| Running | Carrying out its instructions | The operating system decides |
| Blocked | Waiting for a file, a lock or a sleep | time.sleep(), a lock |
| Finished | The function returned | Automatic |
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:
| Piece | What it is for |
|---|---|
Thread | One thread. target, args, daemon, name |
start() | Begins the thread |
join() | Waits for a thread to finish |
Lock | Lets one thread at a time into a section of code |
RLock | A lock the same thread may take again |
Event | One thread waits for a signal from another |
Semaphore | Allows 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.
| Mistake | What you see | Why it happens | Fix |
|---|---|---|---|
Thread(target=fetch()) | The function runs immediately, on the main thread | The brackets called it | target=fetch, args=(...) |
Calling worker.run() | It works, but nothing is concurrent | run() runs it here | Call start() |
Forgetting join() | The program ends before the results arrive | Nothing waited | join() every thread |
args=("Asha") | TypeError about arguments | That is a string, not a tuple | args=("Asha",) |
| Starting a finished thread again | RuntimeError: threads can only be started once | A thread is single-use | Make a new Thread |
| Sharing a counter with no lock | A total that is wrong, sometimes | total += 1 is three steps | with lock: |
| Expecting threads to speed up arithmetic | The same time, or slower | The global interpreter lock | Use multiprocessing |
| A daemon thread writing a file | A half-written file | Daemons are stopped at exit | Do not make it a daemon |
| Relying on the order of output | It changes between runs | The operating system chooses | Collect results, order them after |
| Testing threads in this page | RuntimeError: can't start new thread | A browser tab cannot start one | Run 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-itemargsneeds 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 += 1is 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.
ThreadPoolExecutoris what you should actually reach for once you understandThread.- The browser cannot start a thread, which is why most of this page is code to run on your own computer.
Next: Python References