A game server must handle many players at once. Threads let code run concurrently — but sharing data between them introduces race conditions, the defining hazard of concurrent programming.
A server that handled one player at a time would leave everyone else waiting. Concurrency lets it make progress on many connections at once — receiving one player’s input while sending another’s state. The simplest model is a thread per connection: each runs independently, and the OS interleaves them. This works for moderate scale but, as later lessons show, does not stretch to tens of thousands of connections.
import socket, threading
def handle_client(conn, addr, world):
while True:
data = conn.recv(1024)
if not data: break
world.process(addr, data) # this player's work
# one thread per connection: each runs concurrently, OS interleaves them
server = socket.socket(); server.bind(("0.0.0.0", 7777)); server.listen()
while True:
conn, addr = server.accept()
threading.Thread(target=handle_client, args=(conn, addr, world),
daemon=True).start()
# simple + works to moderate scale. Beyond ~thousands of threads it breaks down
# (memory + context-switch cost) -> event loops (later lessons).The moment two threads touch the same data, you risk a race condition: their operations interleave in an unlucky order and corrupt the state. The classic example is two threads incrementing a shared counter — read, add, write — where both read the same old value and one increment is lost. Races are non-deterministic and hard to reproduce, which makes them the most dangerous class of concurrency bug.
The core hazard required — Any unsynchronized access to shared mutable state from multiple threads is a race condition. Even a single "x += 1" is not atomic — it can lose updates when threads interleave.import threading
counter = 0
def increment():
global counter
for _ in range(100_000):
counter += 1 # NOT atomic: read -> add -> write; threads interleave
threads = [threading.Thread(target=increment) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(counter) # expected 400000, but often LESS — updates were lost!
# two threads read the same old value, both write old+1 -> one increment vanishes.
# races are non-deterministic + rare -> brutal to reproduce. This is THE hazard.Every concurrency bug traces back to shared mutable state accessed without coordination. So the two ways out are: protect shared data with synchronization (next lesson), or avoid sharing altogether by giving each unit of work its own state and communicating through messages (the actor model, last lesson). Recognizing shared mutable state as the root cause is the single most useful concurrency insight.
Every concurrency bug = SHARED MUTABLE STATE touched without coordination.
shared two+ threads reach the same data
mutable at least one of them WRITES it
uncoordinated no rule about who goes when
Two ways to be safe:
1. SYNCHRONIZE guard shared data with locks so only one thread touches it
at a time (next lesson). Correct, but risks deadlock + contention.
2. DON'T SHARE give each worker its OWN state; communicate by passing MESSAGES
(queues / the actor model, last lesson). No races by design.
Great concurrent design minimizes shared mutable state in the first place.A practical wrinkle in some languages: Python’s Global Interpreter Lock lets only one thread run Python bytecode at a time, so threads help with I/O-bound work (waiting on the network) but not CPU-bound work (running game logic in parallel). Other languages (Go, Java, C++, C#) have true parallel threads. The concurrency concepts here are universal; just know your language’s threading model when choosing an approach for a CPU-heavy game server.
Language threading models differ — know yours:
Python the GIL lets only ONE thread run Python bytecode at a time.
-> threads help I/O-bound work (waiting on the network) but NOT
CPU-bound parallelism. For CPU-heavy sim: multiprocessing, C
extensions, or another language. (async is great for I/O — later.)
Go goroutines run truly in parallel across cores (M:N scheduler).
Java/C# real OS threads, parallel; rich concurrency libraries.
C++ std::thread, true parallelism; you manage everything.
The CONCEPTS (races, locks, event loops, actors) are universal. The GIL just
changes WHICH tool you pick for CPU-bound work in Python.