Unlocking Redis 6.0 Multithreaded I/O: How It Works and Boosts Performance
This article explains Redis 6.0's multithreaded I/O feature, covering its background, configuration parameters, execution flow, source code analysis, performance benchmarking against single‑threaded mode, identified limitations, and a brief comparison with Valkey 8.0's advanced I/O design.
Background
Redis is often described as a single‑threaded event‑driven server, but it actually uses a single main thread for command execution while handling network I/O with non‑blocking operations.
Redis 6.0 Multithreaded I/O Overview
Starting with Redis 4.0, background threads were added for asynchronous eviction and large‑key deletion. Redis 6.0 introduced multithreaded I/O, increasing single‑node request capacity from ~100k to ~200k operations per second.
Parameters and Configuration
Two configuration directives control the feature:
<code># io-threads 4 IO thread count
# io-threads-do-reads no Whether reads are also handled by I/O threads</code>io-threads sets the number of I/O threads; values greater than 1 enable multithreading (maximum 128).
io-threads-do-reads defaults to
no; set to
yesto let I/O threads also read and parse client data.
These settings cannot be changed at runtime with
CONFIG SETand are disabled when SSL is enabled.
Execution Flow Overview
The main thread accepts connections and places sockets in a global queue.
After reading, the main thread distributes sockets to I/O threads (or keeps them itself) using round‑robin.
The main thread reads its own assigned data, then waits for I/O threads to finish reading.
I/O threads read and parse data but do not execute commands.
The main thread executes the commands.
Write responses are similarly distributed: the main thread and I/O threads write to clients, then the main thread installs write handlers.
Source Code Analysis
The multithreaded I/O implementation resides in
networking.c. Initialization occurs in
initThreadedIO(), which creates the I/O thread pool based on
io-threads. The main thread calls
initThreadedIO()from
InitServerLast().
<code>/* Initialize the data structures needed for threaded I/O. */
void initThreadedIO(void) {
io_threads_active = 0; /* start with threads inactive */
if (server.io_threads_num == 1) return; /* single‑thread mode */
if (server.io_threads_num > IO_THREADS_MAX_NUM) {
serverLog(LL_WARNING, "Fatal: too many I/O threads configured. The maximum number is %d.", IO_THREADS_MAX_NUM);
exit(1);
}
for (int i = 0; i < server.io_threads_num; i++) {
io_threads_list[i] = listCreate();
if (i == 0) continue; /* thread 0 is the main thread */
pthread_t tid;
pthread_mutex_init(&io_threads_mutex[i], NULL);
io_threads_pending[i] = 0;
pthread_mutex_lock(&io_threads_mutex[i]);
if (pthread_create(&tid, NULL, IOThreadMain, (void*)(long)i) != 0) {
serverLog(LL_WARNING, "Fatal: Can't initialize IO thread.");
exit(1);
}
io_threads[i] = tid;
}
}
</code>The I/O thread main loop (
IOThreadMain) waits for a start condition, processes assigned clients for either read or write operations, and then signals completion by resetting
io_threads_pending.
<code>void *IOThreadMain(void *myid) {
long id = (unsigned long)myid;
char thdname[16];
snprintf(thdname, sizeof(thdname), "io_thd_%ld", id);
redis_set_thread_title(thdname);
redisSetCpuAffinity(server.server_cpulist);
while (1) {
// wait for start condition
for (int j = 0; j < 1000000; j++) {
if (io_threads_pending[id] != 0) break;
}
if (io_threads_pending[id] == 0) {
pthread_mutex_lock(&io_threads_mutex[id]);
pthread_mutex_unlock(&io_threads_mutex[id]);
continue;
}
// process clients
listIter li; listNode *ln;
listRewind(io_threads_list[id], &li);
while ((ln = listNext(&li))) {
client *c = listNodeValue(ln);
if (io_threads_op == IO_THREADS_OP_WRITE) {
writeToClient(c, 0);
} else if (io_threads_op == IO_THREADS_OP_READ) {
readQueryFromClient(c->conn);
} else {
serverPanic("io_threads_op value is unknown");
}
}
listEmpty(io_threads_list[id]);
io_threads_pending[id] = 0;
}
}
</code>Dynamic Pause and Resume
During write handling,
stopThreadedIOIfNeeded()pauses I/O threads when the pending write queue is less than twice the number of threads; otherwise
startThreadedIO()unlocks the mutexes to resume work.
<code>int stopThreadedIOIfNeeded(void) {
int pending = listLength(server.clients_pending_write);
if (server.io_threads_num == 1) return 1;
if (pending < (server.io_threads_num * 2)) {
if (io_threads_active) stopThreadedIO();
return 1;
} else {
return 0;
}
}
</code>Performance Comparison
Benchmarks were run on two 12‑core CentOS machines (1.5 GHz, 256 GB RAM). Using
redis-benchmark, SET/GET throughput roughly doubled when four I/O threads were enabled, especially with many clients and small values. Enabling
io-threads-do-reads=yesgave a modest additional gain.
Conclusion
Redis 6.0’s multithreaded I/O improves network throughput by offloading socket read/write and protocol parsing to worker threads, while command execution remains single‑threaded. Properly configuring thread count and CPU cores can double request capacity, but the design still under‑utilizes CPU resources and lacks TLS support. Valkey 8.0 addresses these gaps with asynchronous I/O and batch memory pre‑fetching, achieving up to 1 M QPS per instance.
Sanyou's Java Diary
Passionate about technology, though not great at solving problems; eager to share, never tire of learning!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.