redis的单线程与多线程

361 阅读3分钟

这章来说说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()函数

这个函数总共分为三部分:

  1. 读取数据
  2. 增加当前数据长度
  3. 处理数据(执行命令)
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,只不过会走下面的单线程方式