# Multithreading

Course: [Python](https://www.devtutorials.ai/learn/python) · Section: Advanced Python
Source: https://www.devtutorials.ai/learn/python/advanced-python/multithreading

> 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:

```python runnable title="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.

```python runnable expect-error title="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:

| | 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 |

```python runnable title="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:

```text
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:

```python runnable title="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.

```text
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:

| 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.

```text
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.

```python runnable title="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.

```text
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.

```python runnable title="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:

```text
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.

```python runnable title="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.

```text
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.

<div className="tscroll">

| 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 |

</div>

## 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**
