redis源码之:多线程与读写事件处理

在redis6之后,引入了多线程,主要是因为硬件的发展,IO设备的吞吐能力在大大增强,很适合同时多任务大批量数据读写。
在了解redis的多线程之前,先来大概看下C语言多线程与metux锁的使用:

void test_thread_mutex();
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int main(int argc, char **argv) {
    test_thread_mutex();
}
void* thread_function(void* arg) {

    pthread_mutex_lock(&mutex); // 获取互斥锁
    sleep(10);
    printf(" new thread! Thread ID: %lu\n", (unsigned long)pthread_self());
    pthread_mutex_unlock(&mutex); // 释放互斥锁
    return NULL;
}
void test_thread_mutex(){
    pthread_t tid;
    pthread_create(&tid, NULL, thread_function, NULL);//创建新线程并在新线程运行function
    printf("Main thread start...\n");
    pthread_mutex_lock(&mutex); // 获取互斥锁
    printf("Main thread processing\n");
    pthread_mutex_unlock(&mutex); // 释放互斥锁

    pthread_join(tid, NULL);
}

代码很简单,如果没了解过,直接看redis多线程的源码有点阻碍。
启动多线程可以根据主机核数配置线程数:redis源码之:多线程与读写事件处理_第1张图片

官方的说明,对于四核主机,可以配置2-3个线程,对于八核主机可以配置6个线程。具体咱也没测过。最多可以配置#define IO_THREADS_MAX_NUM 128个线程。

redis默认是不对读任务进行多任务处理的,如果需要启动读任务多线程处理需要在配置文件配置:
io-threads-do-reads yes

一、多线程初始化:

在main()方法中,进入InitServerLast(),完成bioInit()和initThreadIO():

初始化线程池完成后,缓存多个Io thread,thread2-4被bio线程占用,看名字可能是跟文件关闭、aof持久化等相关的。
redis源码之:多线程与读写事件处理_第2张图片
查看其中一个子线程,可以看到该线程被阻塞:redis源码之:多线程与读写事件处理_第3张图片

二、启动多线程

由多线程初始化可知,最开始,由于主线程抢占了各个子线程的锁,每个子线程都在阻塞状态,因此主线程需要在某处释放锁,启动各个子线程:
通过pthread_mutex_unlock(&io_threads_mutex[id])在IDE里ctrl+G,全局查找释放锁的位置:一个是在子线程本身,一个是在startThreadIO();redis源码之:多线程与读写事件处理_第4张图片
继续跟踪发现,只有handleClientsWithPendingWritesUsingThreads()中调用了startThreadedIO(),handleClientsWithPendingWritesUsingThreads主要的作用是处理pending的写任务,这里主要先看何时启动子线程:
在aeMain()中不断循环遍历获取epoll事件过程中:
redis源码之:多线程与读写事件处理_第5张图片

三、多线程处理读任务

在redis源码之:事件驱动epoll中我们分析过,eventloop不断循环遍历获取事件,然后读事件会通过handler调用到readQueryFromClient(),之前在redis源码之:客户端命令执行Command是以单线程的视角解读,现在来看看多线程下的逻辑:redis源码之:多线程与读写事件处理_第6张图片
那在多线程调用readQueryFromClient()的时候,何时结束子线程的调用,在主线程继续命令执行?在readQueryFromClient()执行到processInputBuffer©:
redis源码之:多线程与读写事件处理_第7张图片
因此可以得出结论,在redis中,可以多线程执行多个client读数据,但是最后还是按client到达的先后顺序执行每个client对应的命令。事务的一致性跟单线程一样的。

对于写数据的场景,主要看handleClientsWithPendingWritesUsingThreads(),套路跟读数据的情况差不多,分多个线程写数据到客户端,不过写数据后续没有命令执行的步骤。这里就不分析了。

四、停止多线程

redis源码之:多线程与读写事件处理_第8张图片
停止多线程的地方主要有两处:

handleClientsWithPendingWritesUsingThreads中停止多线程、通过定时任务在serverCron中尝试关停多线程
redis源码之:多线程与读写事件处理_第9张图片
redis源码之:多线程与读写事件处理_第10张图片
在initServer()的时候,设置了timeEvent,并设置回调函数serverCron().,不断检查当前pending的写任务个数,判断是否暂停多线程。

附:对于写事件的处理

跟上面写事件的跟踪流程差不多。以infoCommand为例,在执行完命令后,会调用addReplyVerbatim()-》addReplyProto()返回响应,里面会调用prepareClientToWrite-》clientInstallWriteHandler()将当前处理的client放到pending_write队列,这也是开启多线程的关键;并将响应信息通过_addReplyToBuffer()写入到client的buffer中,此时并没有写入到fd对应的socket的buffer。redis源码之:多线程与读写事件处理_第11张图片
然后再eventloop的下次循环中,在beforesleep,完成handleClientsWithPendingReadsUsingThreads()后,调用handleClientsWithPendingWritesUsingThreads(),通过多线程对多个client完成writeToClient()调用connWrite()->connSocketWrite(),返回响应到客户端。
在handleClientsWithPendingWritesUsingThreads中通过多线程并行处理多个写任务过程中,有些写任务由于每次写有字节限制,未完成全部数据写的client,会通过connSocketSetWriteHandler()注册写事件的handler(sendReplyToClient())redis源码之:多线程与读写事件处理_第12张图片在epoll中,写事件表示一个文件描述符(通常是套接字)已经准备好进行写操作。当一个套接字的写缓冲区有空间可以写入数据时,会触发写事件,从而调用sendReplyToClient(),完成剩余字节的写。

你可能感兴趣的:(redis源码学习分析,redis,数据库,缓存)