这章来说说redis的单线程和多线程
首先明确一点:redis只有进行网络IO的时候用到了多线程!其他时候都是单线程的!!
也就是说,执行用户的增删改查命令时,都是单线程的,只有最开始read()读socket里的数据,以及write()写回socket的时候是多线程执行的!
截取一段别人的博客,总结的很好:
- Redis 基于内存操作,几乎不存在 CPU 成为瓶颈的情况, 它主要受限于内存和网络,读写网络的 read/write 系统调用占用了 Redis 执行期间大部分 CPU 时间,瓶颈其实主要在于网络的 IO 消耗。基于这种情况,Redis 优化的方向在于提高网络 IO 性能,而一个简单有效的方法就是使用多线程任务分摊 Redis 同步 IO 读写的负荷
- Redis 多线程模型不是把业务逻辑处理交给子线程,而是把对网络数据的读写交给子线程处理,业务逻辑仍然由主线程完成
上面这几段话一定看懂啊,看不懂后文就不用看了
我们把执行网络读写的这些线程叫做IO线程
下面开始分析源码
初始化IO线程
server启动时阶段,InitServerLast()调用initThreadedIO()来初始化IO thread
void initThreadedIO(void) {
server.io_threads_active = 0;
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);
setIOPendingCount(i, 0); // 设置io_threads_pending[i]为0
pthread_mutex_lock(&io_threads_mutex[i]);
pthread_create(&tid,NULL,IOThreadMain,(void*)(long)i);
io_threads[i] = tid;
}
}
- server.io_threads_active表示IO线程是否开始工作(激活),初始化为0,未激活
- server.io_threads_num表示IO线程数
- io_threads_pending[]: 每个 IO 线程待处理的client数量, 都被初始化为0
- io_threads_mutex[]是一个全局的互斥量数组,每个IO线程对应一个,初始化的时候会锁住互斥量,让IO线程阻塞
- io_threads[]记录每个IO线程的线程id 可以看到,initThreadedIO()函数初始化了n个线程执行IOThreadMain()函数,我们去看看IOThreadMain()函数做了什么
IO线程的执行IOThreadMain()
void *IOThreadMain(void *myid) {
long id = (unsigned long)myid;
while(1) {
/* 第一次循环没用,后续循环等待待处理client */
for (int j = 0; j < 1000000; j++) {
if (getIOPendingCount(id) != 0) break;
}
if (getIOPendingCount(id) == 0) {
/* 启动的时候锁住了,第一次运行会阻塞在这里,等待互斥量释放 */
pthread_mutex_lock(&io_threads_mutex[id]);
pthread_mutex_unlock(&io_threads_mutex[id]);
continue;
}
serverAssert(getIOPendingCount(id) != 0);
/*
* 主要处理部分!
*/
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");
}
}
/* 处理完后清空链表,待处理数设为0,回到外层循环等待数据 */
listEmpty(io_threads_list[id]);
setIOPendingCount(id, 0);
}
}
init后,线程阻塞在pthread_mutex_lock()这里
io_threads_list[id]是线程id待处理的client链表,IOThreadMain()的主要逻辑就是当链表不为空时,不断处理这个链表:
- 如果是写,就调用writeToClient(c,0);
- 如果是读,就调用readQueryFromClient(c->conn);
这两个函数在上一章介绍redis执行命令时候说过,这里就不赘述了
IO线程启动
上一节创建了多个IO线程,但是因为锁住了互斥量,线程都是阻塞住的,我们这一节看看IO线程的启动
先记住一个点:待处理的client都存在两个全局变量中
- clients_pending_write[]存了待写的client
- clients_pending_read[]存了待读的client
复习一下上一章<redis处理命令过程>中介绍的
每次事件循环,都会先执行beforeSleep(),beforeSleep() 函数会调用 handleClientsWithPendingWritesUsingThreads()函数处理待返回数据给客户端的client
handleClientsWithPendingWritesUsingThreads()函数如下:
int handleClientsWithPendingWritesUsingThreads(void) {
// 单线程
if (server.io_threads_num == 1 || stopThreadedIOIfNeeded()) {
return handleClientsWithPendingWrites();
}
// 多线程
if (!server.io_threads_active) startThreadedIO();
// 下面是主线程的工作,伪代码
1. 把待处理client从全局变量clients_pending_write
移到每个IO线程该处理的client链表上
item_id = 0;
while(!clients_pending_write.empty()) {
c = clients_pending_write.head;
int target_id = item_id % server.io_threads_num;
listAddNodeTail(io_threads_list[target_id],c);
clients_pending_write.pop_had()
item_id++;
}
...
2. 主线程id是0,处理io_threads_list[0]上的client
listRewind(io_threads_list[0],&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
writeToClient(c,0);
}
3.等待所有IO线程处理完这一波client
while(1) {
unsigned long pending = 0;
for (int j = 1; j < server.io_threads_num; j++)
pending += getIOPendingCount(j);
if (pending == 0) break;
}
4.没有写完就注册回调
listRewind(server.clients_pending_write,&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
/* Install the write handler if there are pending writes in some
* of the clients. */
if (clientHasPendingReplies(c) &&
connSetWriteHandler(c->conn, sendReplyToClient) == AE_ERR)
{
freeClientAsync(c);
}
}
listEmpty(server.clients_pending_write);
/* Update processed count on server */
server.stat_io_writes_processed += processed;
return processed;
}
server.io_threads_num 是用户配置的线程数,决定了是否启用多线程,如果是1就不启动,否则启动多线程IO,startThreadedIO()启动多线程
startThreadedIO()
void startThreadedIO(void) {
for (int j = 1; j < server.io_threads_num; j++)
pthread_mutex_unlock(&io_threads_mutex[j]);
server.io_threads_active = 1;
}
可以看到,启动就是把每个线程的互斥量给释放掉,这样每个IO线程就可以在IOThreadMain()中继续往下执行了,也就是不断处理。
主线程也不会停下傻等,回处理io_threads_list[0]上的client
IO线程停止
stopThreadedIOIfNeeded()
stopThreadedIOIfNeeded()会检测当前情况是否还需要多线程,如果不需要就关闭多线程模式
int stopThreadedIOIfNeeded(void) {
int pending = listLength(server.clients_pending_write);
/* Return ASAP if IO threads are disabled (single threaded mode). */
if (server.io_threads_num == 1) return 1;
if (pending < (server.io_threads_num*2)) {
if (server.io_threads_active) stopThreadedIO();
return 1;
} else {
return 0;
}
}
关闭的条件是:当前待处理(发送)client数量小于线程数的两倍
如果满足条件,就调用stopThreadedIO()关闭
调用stopThreadedIOIfNeeded()检测是否需要关闭多线程的地方有这几个:
- serverCron()函数,redis的定时任务
- handleClientsWithPendingWritesUsingThreads():发送client数据的时候
stopThreadedIO
stopThreadedIO()函数用来关闭多线程模式
void stopThreadedIO(void) {
handleClientsWithPendingReadsUsingThreads();
serverAssert(server.io_threads_active == 1);
for (int j = 1; j < server.io_threads_num; j++)
pthread_mutex_lock(&io_threads_mutex[j]);
server.io_threads_active = 0;
}
- 先处理掉clients_pending_read上剩余的client
- 然后互斥量上锁
读时的多线程
上一章<redis处理命令过程>中介绍了readQueryFromClient()函数
这个函数总共分为三部分:
- 读取数据
- 增加当前数据长度
- 处理数据(执行命令)
void readQueryFromClient(connection *conn) {
client *c = connGetPrivateData(conn);
int nread, readlen;
size_t qblen;
if (postponeClientRead(c)) return;
....
}
我们关注一开始这句postponeClientRead(c)
postponeClientRead()
int postponeClientRead(client *c) {
if (server.io_threads_active &&
server.io_threads_do_reads &&
!ProcessingEventsWhileBlocked &&
!(c->flags & (CLIENT_MASTER|CLIENT_SLAVE|CLIENT_PENDING_READ)))
{
c->flags |= CLIENT_PENDING_READ;
listAddNodeHead(server.clients_pending_read,c);
return 1;
} else {
return 0;
}
}
这个函数的作用:如果开启了多线程模式,就把client加入全局待处理读client链表 clients_pending_read 中,然后直接return,后续IO线程会去处理,IO线程调用的还是readQueryFromClient,只不过会走下面的单线程方式