Python
GIL
Multi-threading
Locks
Concurrency

Are locks unnecessary in multi-threaded Python code because of the GIL?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

The GIL in CPython does not eliminate the need for locks. It only ensures that one thread executes Python bytecode at a time inside the interpreter. If multiple threads access shared mutable state, you still need synchronization whenever correctness depends on a multi-step operation happening atomically.

What the GIL Actually Protects

The GIL is an interpreter-level lock, not an application-level correctness guarantee. It prevents simultaneous execution of Python bytecode in multiple threads, but threads can still interleave between bytecode instructions. That means a logical operation such as incrementing a counter can be interrupted halfway through.

A high-level line like counter += 1 is not one indivisible application event. It involves reading, computing, and writing.

Race Condition Example

This code looks simple, but it can still produce the wrong result under threading.

python
1import threading
2
3counter = 0
4
5def worker(n):
6    global counter
7    for _ in range(n):
8        counter += 1
9
10threads = [threading.Thread(target=worker, args=(100_000,)) for _ in range(4)]
11for t in threads:
12    t.start()
13for t in threads:
14    t.join()
15
16print(counter)

The GIL does not guarantee that the final value will be exactly 400000. The problem is a race on shared state, not memory corruption in the C sense.

Use threading.Lock for Shared Mutable State

When multiple threads update the same value or container, protect the critical section with a lock.

python
1import threading
2
3counter = 0
4lock = threading.Lock()
5
6def worker(n):
7    global counter
8    for _ in range(n):
9        with lock:
10            counter += 1
11
12threads = [threading.Thread(target=worker, args=(100_000,)) for _ in range(4)]
13for t in threads:
14    t.start()
15for t in threads:
16    t.join()
17
18print(counter)

This serializes the update so the shared invariant stays correct.

Some Built-Ins Are Not a Complete Safety Story

Developers sometimes hear that operations on lists or dicts are "thread-safe" in CPython. That statement is easy to misuse. Some individual operations may be implemented safely at the interpreter level, but compound logic is still vulnerable.

For example, this pattern is unsafe without a lock:

  • check whether a key exists
  • compute a new value
  • update the dictionary

Another thread can modify the dictionary between those steps.

Use Higher-Level Thread-Safe Patterns When Possible

Locks are important, but sometimes the best solution is to avoid shared mutable state altogether. A queue is often better than a shared list for producer-consumer work.

python
1import queue
2import threading
3
4q = queue.Queue()
5
6for i in range(5):
7    q.put(i)
8
9def consumer():
10    while True:
11        try:
12            item = q.get_nowait()
13        except queue.Empty:
14            return
15        print(item)
16        q.task_done()
17
18threads = [threading.Thread(target=consumer) for _ in range(2)]
19for t in threads:
20    t.start()
21for t in threads:
22    t.join()

queue.Queue removes much of the manual locking burden and makes ownership clearer.

CPU-Bound Threads Still Do Not Scale Well

The GIL also matters for performance. For I O-bound work, threads are often useful because blocking calls can release the GIL. For CPU-bound pure Python work, threads do not give true parallel speedup on multiple cores in CPython.

For CPU-bound tasks, prefer multiprocessing, native extensions, or vectorized libraries that release the GIL internally.

Common Pitfalls

  • Assuming the GIL makes all shared-state access automatically safe.
  • Treating counter += 1 as one atomic application action.
  • Trusting built-in container operations without checking whether the whole workflow is multi-step.
  • Using threads for CPU-bound speedups in pure Python.
  • Reaching for locks everywhere when a queue or ownership redesign would be simpler.

Summary

  • The GIL is not a replacement for locks.
  • Shared mutable state still needs synchronization when multiple threads can interleave.
  • Use threading.Lock for critical sections that must be atomic.
  • Prefer thread-safe patterns such as queues when they fit the problem.
  • Use threads mainly for I O-bound work in CPython, not for CPU-bound parallelism.

Course illustration
Course illustration

All Rights Reserved.