Skip to content

Threading Model

irrld edited this page Jul 30, 2026 · 4 revisions

znet owns its threads. You never start one, and your code runs on them rather than on the thread that created the server or client. This page is the contract.

The threads

Client: two, plus whatever the transport runs.

Thread Runs
Loop Drives the session: reads, dispatches your handlers, flushes
Encoder Serializes queued packets so encoding overlaps the flush

Server: one acceptor plus a worker pool.

Thread Runs
Acceptor Accepts connections, drives sessions until their handshake finishes, then hands each to a worker
Worker Drives its assigned sessions: reads, dispatches your handlers, encodes and flushes

A ZDT server adds a receive thread that takes datagrams off the shared UDP socket and routes them to the right session.

Workers get sessions by least-loaded assignment at promotion. A session stays with its worker for life, so all of one session's callbacks are serialized. But a session changes threads once, from the acceptor to its worker, when its handshake completes.

What calls your code

Your code Called on
Event callback The thread that owns the session: a worker, the acceptor for a not-yet-promoted session, or the client's loop
OnPacket handlers The same
SerializeTyped Whichever thread encodes: the client's encoder, or a server worker
DeserializeTyped The session's owning thread

What that means in practice

One session never runs two callbacks at once. You do not need a lock to protect state belonging to a single session.

Different sessions run concurrently, on different workers. Anything shared across sessions, a room list, a player registry, a shared codec, is touched concurrently and needs its own synchronization. A shared Codec is safe because serializers are stateless and it is not mutated after setup; mutating one while sessions are live is not.

Your handler blocks its session's thread. On a server that thread is also driving other sessions, so a slow handler delays them too. Hand long work to your own thread pool.

A session can move between threads once. The acceptor drives it during the handshake; a worker drives it afterwards. Since the two never overlap, ordinary non-atomic state is fine. But code that latches a thread id at construction and asserts it later will be wrong.

What you may call, and from where

SendPacket is safe from any thread. It only queues, and the queue is lock-free precisely so a game loop calling it never blocks behind a worker mid-encode. Check the return value: false means full.

Close is safe from any thread.

SetCodec and SetHandler are meant to be called from the session's own thread, which is what you get inside the connected event or inside an OnPacket. Calling them from elsewhere while the session is live races the thread that reads them.

metrics() is a snapshot and safe to read from any thread. Values may be slightly stale; see Metrics.

Ordering

Messages sent on one session arrive in the order you called SendPacket, per channel, when the delivery mode says ordered. Sends from two threads to the same session interleave in whatever order they reach the queue. The queue preserves order, but "which came first" is decided by your threads, not by znet.

If ordering between two messages matters, send them from the same thread.

Lifetime

Sessions are shared_ptr. Holding one past a disconnect is safe: it stays valid and IsAlive() returns false. This matters most in P2P, where the locator returns and you keep the session it produced.

Destroying a Server or Client stops and joins its threads, so no callback can be running once the destructor returns. Do not destroy either from inside its own callback.

Debug assertions

Debug builds check ZDT's thread rules at runtime: entering the transport's update, flush or receive path from two threads at once aborts with two threads entered one ZDT thread domain at once. These compile out entirely in release, since the guard member itself is #ifndef NDEBUG, so object layout is unchanged.

If you are doing something unusual with threads, run your tests against a debug build of znet at least once.

Clone this wiki locally