σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademySkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
Skills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python skill›Module 10 · High-performance Python engineering›Topic 10.9·~55 minAdvanced

Threads and the GIL

Skill positionModule 10 of 11 · Topic 9 of 13
10.9 Threads and the GIL
—Lab score— / 100Total XP—Streak—

Threads: concurrency inside one process

A thread is an independent line of execution inside one program, sharing its memory with the other threads. A trading program that must fetch quotes from ten venues, write a log and keep a UI responsive uses threads so that waiting for one does not stop the rest. Python's threading module and, more conveniently, concurrent.futures.ThreadPoolExecutor start and manage them. Because the threads share variables, they are fast to communicate and dangerous to get wrong.

# Ten slow "network calls" in a thread pool wait at the same time, so the total is about one call, not ten
import time
from concurrent.futures import ThreadPoolExecutor

def fetch_quote(symbol):
    time.sleep(0.1)                                  # stands in for waiting on a venue
    return symbol, len(symbol) * 10.0

symbols = ["AAPL", "MSFT", "NVDA", "TSLA", "AMZN", "META", "GOOG", "NFLX", "ORCL", "INTC"]

start = time.perf_counter()
with ThreadPoolExecutor(max_workers=10) as pool:
    quotes = list(pool.map(fetch_quote, symbols))
elapsed = time.perf_counter() - start

print(quotes[0], len(quotes))                        # ('AAPL', 40.0) 10
print(elapsed < 0.5)                                 # True  about 0.1 s in total, not 10 x 0.1 s

The GIL: threads that wait, and threads that compute

CPython has a Global Interpreter Lock (GIL): only one thread executes Python bytecode at any moment. A thread gives it up while it waits for the network, a file or time.sleep, so threads overlap waiting very well: ten threads fetching quotes finish in about the time of one. But for CPU-bound Python code (a pure-Python loop doing arithmetic) the threads take turns, so four threads are no faster than one, and often a little slower. Code that runs outside Python, such as NumPy or a C extension that releases the GIL, can run in parallel in threads. For heavy pure-Python computation, use separate processes (next topic), or the free-threaded build of Python 3.13+ that removes the GIL, where available.

# Splitting a list into chunks for workers: sizes differ by at most one, the first chunks get the extra items
def split_chunks(items, workers):
    size, extra = divmod(len(items), workers)
    chunks, start = [], 0
    for index in range(workers):
        stop = start + size + (1 if index < extra else 0)
        chunks.append(list(items[start:stop]))
        start = stop
    return chunks

chunks = split_chunks(list(range(7)), 3)
print(chunks)                                        # [[0, 1, 2], [3, 4], [5, 6]]
print([sum(chunk) for chunk in chunks])              # [3, 7, 11]   one partial sum per worker
print(sum(sum(chunk) for chunk in chunks))           # 21

Shared data, race conditions and locks

Threads that update the same variable can corrupt it, because total += amount is three steps (read, add, write) and a thread switch may happen between them: two threads both read 100, both add 5, both write 105, and one update is lost. This is a race condition, and it appears rarely and unpredictably, which makes it the worst kind of bug. The fix is a lock: with lock: lets one thread at a time run the block, and the read and the write must both be inside it. Better still, avoid sharing: give each worker its own chunk and its own result, and combine the results at the end, or pass work and results through a queue.Queue, which is already thread-safe.

# A lock makes the read-modify-write a single step; the total comes out exact
import threading

totals = {"AAPL": 0}
lock = threading.Lock()

def safe_add(shared, key, amount, lock):
    with lock:
        shared[key] = shared.get(key, 0) + amount
        return shared[key]

def worker():
    for _ in range(10_000):
        safe_add(totals, "AAPL", 1, lock)

threads = [threading.Thread(target=worker) for _ in range(4)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

print(totals["AAPL"])                                # 40000  nothing lost: four threads x 10,000 additions
# A back-of-envelope estimate before writing any threaded code
import math

def estimate_time(n_tasks, seconds, workers, kind):
    if kind in ("io", "processes"):
        return math.ceil(n_tasks / workers) * seconds
    return n_tasks * seconds                         # "cpu_threads": the GIL makes the threads take turns

print(estimate_time(40, 0.5, 8, "io"))               # 2.5    forty slow requests, eight at a time
print(estimate_time(40, 0.5, 8, "cpu_threads"))      # 20.0   forty CPU-bound tasks in threads: no gain
print(estimate_time(40, 0.5, 8, "processes"))        # 2.5    separate processes use separate cores
Threads are the wrong tool more often than you thinkThreads fit I/O: many slow, independent waits. For many connections in one process, asyncio (earlier in this skill) usually scales better and has no races between awaits. For arithmetic on arrays, vectorise with NumPy. For CPU-bound Python, use processes. If you do share data between threads, keep the locked section short, never call out to slow code while holding a lock, and take several locks in the same order everywhere or two threads can wait for each other forever (a deadlock).
  • Threads share memory and overlap waiting well (network, files); one ThreadPoolExecutor can fetch from many venues at once.
  • The GIL lets one thread run Python bytecode at a time: no speed-up for CPU-bound pure Python, but NumPy and C extensions can release it.
  • Protect shared updates with with lock: around the read and the write, prefer not sharing (chunks, queues), and watch for deadlocks.

Next up: multiprocessing, using several processes to put every CPU core to work.

Lab · what's graded

  • Share the work out with split_chunks25 pts
  • Map a function over the chunks with run_in_chunks25 pts
  • Protect shared data with safe_add25 pts
  • Know what threads buy you with estimate_time25 pts
Explorer · lab-125-1
▾ lab-125-1
…
bash · in-browser
learner@quantcraft:~/lab-125-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-125-1 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes

Log in to run your code.

Every lecture is free to read. Create an account or log in to run code, run tests and submit this lab.

  • In-browser IDE with a bash terminal — nothing to install
  • Instant grading, XP and your place on the skill
Create an accountLog in
OR
Continue with GitHub