Thread Safety Considerations When Using XQUIC: A Complete Developer's Guide
XQUIC is strictly single-threaded; all engine, connection, and stream operations must occur on the thread that created the engine, requiring external synchronization for any cross-thread data sharing.
XQUIC, Alibaba's high-performance QUIC and HTTP/3 implementation, follows a single-threaded event-driven architecture that demands careful attention to thread safety considerations when using XQUIC in multi-threaded applications. Unlike libraries that provide internal locking, XQUIC requires the application to serialize all API calls per engine instance. This guide examines the threading constraints documented in include/xquic/xquic.h and docs/API.md, providing practical patterns for safe integration.
Core Threading Model (Single-Threaded Design)
XQUIC is designed as a single-threaded library with no internal locking mechanisms. All core objects—including the engine (xqc_engine_t), connections, streams, and the majority of public APIs—must be accessed only from the thread that owns the engine.
According to docs/API.md, "Application can create one or more engines in a process, but multiple engines MUST NOT share one thread, as XQUIC is single-threaded." This constraint is enforced by design rather than runtime checks; concurrent access from multiple threads results in race conditions and undefined behavior.
Critical Thread Safety Constraints
One Engine Per Thread Rule
Each engine instance must be bound to a dedicated thread for its entire lifecycle. The xqc_engine_create function establishes this binding, and all subsequent calls—including xqc_engine_packet_process, xqc_engine_main_logic, and xqc_engine_finish_recv—must occur on that same thread.
Running multiple engines on a single thread is explicitly prohibited. If your application requires multiple engines, assign each to a separate thread with proper isolation.
Callback Execution Context
All engine-layer callbacks—including timer handlers, socket write functions, and logging callbacks—are invoked synchronously from the engine's thread. As implemented in src/transport/xqc_engine.c, these callbacks execute without any internal serialization.
Do not spawn new threads inside a callback unless you first copy required data and protect it with your own synchronization primitives. Blocking operations within callbacks stall the entire engine, delaying packet processing and timer handling.
Non-Thread-Safe APIs
Specific functions are explicitly marked as non-thread-safe in the public headers. For example, include/xquic/xquic.h states: "This function is not thread‑safe." regarding xqc_log_disable.
Call such configuration functions before the engine starts processing packets, or guard them with an external lock if runtime modification is necessary. The xqc_set_event_timer_pt function and other setup routines follow the same pattern.
Practical Implementation Patterns
Single-Threaded Event Loop
The canonical usage pattern involves creating the engine on a dedicated thread and running an event loop that processes packets and timers sequentially:
/* 1️⃣ Create the engine – done once, on the dedicated thread */
xqc_engine_t *engine = xqc_engine_create(
XQC_ENGINE_CLIENT, /* role */
&engine_cfg, /* config */
&ssl_cfg, /* TLS config */
&engine_cbs, /* callbacks (timer, log, etc.) */
NULL); /* user_data */
/* 2️⃣ Register ALPN (e.g. HTTP/3) */
xqc_engine_register_alpn(engine, "h3", 2, &h3_cbs);
/* 3️⃣ Main event loop – single thread! */
while (running) {
/* a) read UDP packets from the socket */
ssize_t n = recvfrom(sock, buf, sizeof(buf), 0,
(struct sockaddr *)&peer, &peerlen);
if (n > 0) {
/* b) hand the packet to XQUIC */
xqc_engine_packet_process(engine, buf, n,
(struct sockaddr *)&local, local_len,
(struct sockaddr *)&peer, peer_len,
now_usec(), NULL);
}
/* c) let XQUIC run its timers and state machines */
xqc_engine_main_logic(engine);
/* d) optional: finish batch processing */
xqc_engine_finish_recv(engine);
}
If you need background work (e.g., file I/O or database access), run those tasks on separate threads and communicate results back to the XQUIC thread via a lock-free queue or a simple pipe() that the XQUIC thread polls.
Protecting Shared Resources
When application-level objects must be accessed from both the XQUIC thread and other worker threads, use external synchronization primitives:
typedef struct {
uint64_t total_bytes_sent;
uint64_t total_bytes_recv;
pthread_mutex_t lock; /* <-- external mutex */
} app_stats_t;
app_stats_t g_stats = {0, 0, PTHREAD_MUTEX_INITIALIZER};
/* XQUIC stream-write callback (runs on XQUIC thread) */
static ssize_t my_write_cb(const unsigned char *buf, size_t size,
const struct sockaddr *peer, socklen_t peerlen,
void *conn_user_data)
{
/* Send data to the network … */
/* Update shared stats – protect with mutex because other threads read it */
pthread_mutex_lock(&g_stats.lock);
g_stats.total_bytes_sent += size;
pthread_mutex_unlock(&g_stats.lock);
return (ssize_t)size;
}
/* Another worker thread that periodically logs the stats */
void *stat_printer(void *arg)
{
while (1) {
sleep(5);
pthread_mutex_lock(&g_stats.lock);
printf("Sent=%lu, Recv=%lu\n",
g_stats.total_bytes_sent, g_stats.total_bytes_recv);
pthread_mutex_unlock(&g_stats.lock);
}
return NULL;
}
Key Source Files and Functions
Understanding the threading model requires familiarity with these specific files in the alibaba/xquic repository:
| File | Relevance to Thread Safety |
|---|---|
include/xquic/xquic.h |
Contains explicit "not thread‑safe" annotations and public API declarations for xqc_engine_create, xqc_log_disable, and xqc_set_event_timer_pt. |
docs/API.md |
Documents the single‑threaded constraint and the rule that multiple engines must not share one thread. |
src/transport/xqc_engine.c |
Implements xqc_engine_main_logic, xqc_engine_packet_process, and xqc_engine_finish_recv; all internal data structures are accessed without locks. |
src/transport/xqc_timer.c |
Defines timer callback expectations; the user-supplied timer must invoke xqc_engine_main_logic on the engine's thread. |
Summary
- XQUIC is strictly single-threaded: All API calls, callbacks, and timer handlers for a given engine must execute on the thread that created the engine.
- One engine per thread: Multiple engines in the same process require dedicated threads; never run multiple engines on a single thread.
- No internal locking: The library accesses internal data structures without mutexes; concurrent access from multiple threads causes undefined behavior.
- Callbacks are synchronous: Engine callbacks execute on the engine thread; avoid blocking operations or spawn worker threads for heavy tasks, communicating results back via queues.
- External synchronization required: Protect any shared application data accessed from both XQUIC callbacks and other threads with mutexes, atomics, or message passing.
Frequently Asked Questions
Is XQUIC thread-safe?
No, XQUIC is explicitly not thread-safe by design. According to the source code in include/xquic/xquic.h and documentation in docs/API.md, the library operates as a single-threaded engine. All functions—including xqc_engine_packet_process, xqc_engine_main_logic, and connection callbacks—must be called from the single thread that owns the engine instance.
Can I share an XQUIC engine between multiple threads?
No, you cannot share an XQUIC engine between threads. The engine must remain on the thread where it was created via xqc_engine_create for its entire lifecycle. While you can create multiple engines in a single process, docs/API.md explicitly states that multiple engines must not share one thread. Each engine requires its own dedicated thread with proper isolation.
How do I handle blocking I/O operations in XQUIC callbacks?
You must never perform blocking operations inside XQUIC callbacks such as socket write handlers or timer callbacks, as these execute synchronously on the engine thread and will stall packet processing. Instead, offload blocking work (file I/O, database queries) to separate worker threads. Communicate results back to the XQUIC thread via lock-free queues, pipes, or other thread-safe mechanisms that the engine can poll in its main event loop.
Are XQUIC configuration functions thread-safe?
Most XQUIC configuration functions are not thread-safe, particularly those that modify global or engine-wide settings. For example, xqc_log_disable is explicitly marked in include/xquic/xquic.h as "not thread-safe." You should configure the engine—setting parameters, registering ALPNs, and adjusting log settings—before starting packet processing, or protect these calls with external mutexes if runtime modification is absolutely necessary.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →