Libuv源码分析 —— 8. 线程池

网络I/O

  • 上一节 的学习中,我们已经搞明白了网络I/O的基本过程,并通过了解进程/线程间通信来熟悉这个流程。下面,让咱们学习线程池中的线程如何工作、并和主进程进行通信的吧!

image.png

线程池

  • Libuv 是基于事件驱动的异步库。对于耗时的操作。如果在 Libuv 的主循环里执行的话, 就会阻塞后面的任务执行。所以 Libuv 里维护了一个线程池。他负责处理 Libuv 中耗时 的操作,比如文件 io、dns、用户自定义的耗时任务(文件 io 因为存在跨平台兼容的问 题。无法很好地在事件驱动模块实现异步 io)
  • 线程池是全局的,并且在所有事件循环中共享
Thread pool work scheduling
数据类型
  • uv_work_t

    工作请求类型。

  • void (*uv_after_work_cb)uv_work_t *  req , int 状态

    uv_queue_work()在线程池上的工作完成后,将在循环线程上调用的回调。如果工作被取消使用状态将是。uv_cancel() UV_ECANCELED

API
  • int uv_queue_work(uv_loop_t* loopuv_work_t* requv_work_cb work_cbuv_after_work_cb after_work_cb)

    初始化一个工作请求,它将在线程池中的一个线程中运行给定的work_cb。一旦work_cb完成,将在循环线程上调用after_work_cb 。

    可以使用 取消此请求uv_cancel()

example
  • #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    #include <uv.h>
    
    #define FIB_UNTIL 25
    uv_loop_t *loop;
    
    long fib_(long t) {
        if (t == 0 || t == 1)
            return 1;
        else
            return fib_(t-1) + fib_(t-2);
    }
    
    // 将在不同的函数中运行
    void fib(uv_work_t *req) {
        int n = *(int *) req->data;
        if (random() % 2)
            sleep(1);
        else
            sleep(3);
        long fib = fib_(n);
        fprintf(stderr, "%dth fibonacci is %lu\n", n, fib);
    }
    
    void after_fib(uv_work_t *req, int status) {
        fprintf(stderr, "Done calculating %dth fibonacci\n", *(int *) req->data);
    }
    
    /*
        我们将要执行fibonacci数列,并且睡眠一段时间,将阻塞和cpu占用时间长的任务分配
        到不同的线程,使得其不会阻塞event loop上的其他任务
    */
    int main() {
        loop = uv_default_loop();
    
        int data[FIB_UNTIL];
        uv_work_t req[FIB_UNTIL];   // 子线程的参数
        int i;
    
        for (i = 0; i < FIB_UNTIL; i++) {
            data[i] = i;
            // 可以通过void *data传递任何数据,使用它来完成线程之间的沟通任务
            req[i].data = (void *) &data[i];
            uv_queue_work(loop, &req[i], fib, after_fib);
        }
    
        return uv_run(loop, UV_RUN_DEFAULT);
    }
    
    /*
        执行结果
        0th fibonacci is 1
        2th fibonacci is 2
        3th fibonacci is 3
        Done calculating 0th fibonacci
        Done calculating 2th fibonacci
        Done calculating 3th fibonacci
        4th fibonacci is 5
        5th fibonacci is 8
        Done calculating 4th fibonacci
        Done calculating 5th fibonacci
        1th fibonacci is 1
        Done calculating 1th fibonacci
        8th fibonacci is 34
        Done calculating 8th fibonacci
        9th fibonacci is 55
        Done calculating 9th fibonacci
        6th fibonacci is 13
        Done calculating 6th fibonacci
        11th fibonacci is 144
        Done calculating 11th fibonacci
        7th fibonacci is 21
        Done calculating 7th fibonacci
        13th fibonacci is 377
        Done calculating 13th fibonacci
        14th fibonacci is 610
        Done calculating 14th fibonacci
        10th fibonacci is 89
        Done calculating 10th fibonacci
        12th fibonacci is 233
        Done calculating 12th fibonacci
        15th fibonacci is 987
        Done calculating 15th fibonacci
        16th fibonacci is 1597
        17th fibonacci is 2584
        Done calculating 16th fibonacci
        Done calculating 17th fibonacci
        18th fibonacci is 4181
        Done calculating 18th fibonacci
        20th fibonacci is 10946
        Done calculating 20th fibonacci
        22th fibonacci is 28657
        Done calculating 22th fibonacci
        23th fibonacci is 46368
        Done calculating 23th fibonacci
        19th fibonacci is 6765
        Done calculating 19th fibonacci
        21th fibonacci is 17711
        Done calculating 21th fibonacci
        24th fibonacci is 75025
        Done calculating 24th fibonacci
    */
    
线程间的同步原语
Mutex锁

互斥锁用于对资源的互斥访问,当你访问的内存资源可能被别的线程访问到,这个时候你就可以考虑使用互斥锁,在访问的时候锁住。对应的使用流程可能是这样的:

  • 初始化互斥锁:uv_mutex_init(uv_mutex_t* handle)
  • 锁住互斥资源:uv_mutex_lock(uv_mutex_t* handle)
  • 解锁互斥资源:uv_mutex_unlock(uv_mutex_t* handle)
读写锁
信号量

信号量是一种专门用于提供不同进程间或线程间同步手段的原语。信号量本质上是一个非负整数计数器,代表共享资源的数目,通常是用来控制对共享资源的访问。一般使用步骤是这样的:

  • 初始化信号量:int uv_sem_init(uv_sem_t* sem, unsigned int value)
  • 信号量加1:void uv_sem_wait(uv_sem_t* sem)
  • 信号量减1:void uv_sem_post(uv_sem_t* sem)
  • 信号量销毁:void uv_sem_wait(uv_sem_t* sem)
条件变量

条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足。条件变量的内部实质上是一个等待队列,放置等待(阻塞)的线程,线程在条件变量上等待和通知,互斥锁用来保护等待队列(因为所有的线程都可以放入等待队列,所以等待队列成为了一个共享的资源,需要被上锁保护),因此条件变量通常和互斥锁一起使用。一般使用步骤是这样的:

  • 初始化条件变量:int uv_cond_init(uv_cond_t* cond)
  • 线程阻塞等待被唤醒:void uv_cond_wait(uv_cond_t cond, uv_mutex_t mutex)
  • 别的线程唤醒阻塞的线程:void uv_cond_signal(uv_cond_t* cond)
屏障

在多线程的时候,我们总会碰到一个需求,就是需要等待一组进程全部执行完毕后再执行某些事,由于多线程是乱序的,无法预估线程都执行到哪里了,这就要求我们有一个屏障作为同步点,在所有有屏障的地方都会阻塞等待,直到所有的线程都的代码都执行到同步点,再继续执行后续代码。使用步骤一般是:

  • 初始化屏障需要达到的个数:int uv_barrier_init(uv_barrier_t* barrier, unsigned int count)
  • 每当达到条件便将计数+1:int uv_barrier_wait(uv_barrier_t* barrier)
  • 销毁屏障:void uv_barrier_destroy(uv_barrier_t* barrier)

源码解析

init_threads
  • 线程池的初始化主要是初始化一些数据结构,然后创建多个线程。接着在每个线程里执行 worker 函数
  • 源码
    static void init_threads(void) {
      unsigned int i;
      const char* val;
      uv_sem_t sem;
    
      // 默认线程数 4 个,static uv_thread_t default_threads[4];
      nthreads = ARRAY_SIZE(default_threads);
      // 判断用户是否在环境变量中设置了线程数,是的话取用户定义的
      val = getenv("UV_THREADPOOL_SIZE");
      if (val != NULL)
        nthreads = atoi(val);
      if (nthreads == 0)
        nthreads = 1;
    
      // #define MAX_THREADPOOL_SIZE 128 最多 128 个线程
      if (nthreads > MAX_THREADPOOL_SIZE)
        nthreads = MAX_THREADPOOL_SIZE;
    
      threads = default_threads;
    
      // 超过默认大小,重新分配内存
      if (nthreads > ARRAY_SIZE(default_threads)) {
        threads = uv__malloc(nthreads * sizeof(threads[0]));
        // 分配内存失败,回退到默认
        if (threads == NULL) {
          nthreads = ARRAY_SIZE(default_threads);
          threads = default_threads;
        }
      }
    
      // 初始化条件变量
      if (uv_cond_init(&cond))
        abort();
    
      // 初始化互斥变量
      if (uv_mutex_init(&mutex))
        abort();
    
      // 初始化三个队列
      QUEUE_INIT(&wq);
      QUEUE_INIT(&slow_io_pending_wq);
      QUEUE_INIT(&run_slow_work_message);
    
      // 初始化信号量变量,值为 0
      if (uv_sem_init(&sem, 0))
        abort();
    
      // 创建多个线程,工作函数为 worker,sem 为 worker 入参
      for (i = 0; i < nthreads; i++)
        if (uv_thread_create(threads + i, worker, &sem))
          abort();
    
      // 为 0 则阻塞,非 0 则减一,这里等待所有线程启动成功再往下执行
      for (i = 0; i < nthreads; i++)
        uv_sem_wait(&sem);
    
      uv_sem_destroy(&sem);
    }
    
uv__work_submit — 给线程池提交一个任务
  • uv_work
    struct uv__work {
     void (*work)(struct uv__work *w);
     void (*done)(struct uv__work *w, int status);
     struct uv_loop_s* loop;
     void* wq[2];
    };
    
  • 源码
void uv__work_submit(uv_loop_t* loop,
                    struct uv__work* w,
                    enum uv__work_kind kind,
                    void (*work)(struct uv__work* w),
                    void (*done)(struct uv__work* w, int status)) {
 // 保证已经初始化线程,并只执行一次,所以线程池是在提交第一个任务的时候才被初始化
 uv_once(&once, init_once);
 w->loop = loop;
 w->work = work;
 w->done = done;
 // 调 post 往线程池的队列中加入一个新的任务。Libuv 把任务分为三种类型,慢
 // io(dns 解析)、快 io(文件操作)、cpu 密集型等,kind 就是说明任务的类型的
 post(&w->wq, kind);
}

static void init_once(void) {
   #ifndef _WIN32
     if (pthread_atfork(NULL, NULL, &reset_once))
       abort();
   #endif
     init_threads();
}

// 把任务插入队列等待线程处理
static void post(QUEUE* q, enum uv__work_kind kind) {
 // 加锁访问任务队列,因为这个队列是线程池共享的
 uv_mutex_lock(&mutex);

 // 类型是慢 IO
 if (kind == UV__WORK_SLOW_IO) {
   /* 
     插入慢 IO 对应的队列,llibuv 这个版本把任务分为几种类型,
     对于慢 io 类型的任务,libuv 是往任务队列里面插入一个特殊的节点
     run_slow_work_message,然后用 slow_io_pending_wq 维护了一个慢 io 任务的队列,
     当处理到 run_slow_work_message 这个节点的时候,libuv 会从 slow_io_pending_wq
     队列里逐个取出任务节点来执行。
   */
   QUEUE_INSERT_TAIL(&slow_io_pending_wq, q);
   /*
     有慢 IO 任务的时候,需要给主队列 wq 插入一个消息节点 run_slow_work_message,
     说明有慢 IO 任务,所以如果 run_slow_work_message 是空,说明还没有插入主队列。
     需要进行 q = &run_slow_work_message;赋值,然后把 run_slow_work_message 插入
     主队列。如果 run_slow_work_message 非空,说明已经插入线程池的任务队列了。
     解锁然后直接返回。
   */
   if (!QUEUE_EMPTY(&run_slow_work_message)) {
     /* Running slow I/O tasks is already scheduled => Nothing to do here.
        The worker that runs said other task will schedule this one as well. */
     uv_mutex_unlock(&mutex);
     return;
   }

   // 说明 run_slow_work_message 还没有插入队列,准备插入队列
   q = &run_slow_work_message;
 }

 // 把节点插入主队列,可能是慢 IO 消息节点(如果遍历这个队列发现是消息节点
 // 就可以执行 slow_io_pending_wq 队列里的任务了)或者一般任务(直接执行)
 QUEUE_INSERT_TAIL(&wq, q);
 
 // 有空闲线程则唤醒他,如果大家都在忙,则等到他忙完后就会重新判断是否还有新任务
 if (idle_threads > 0)
   uv_cond_signal(&cond);
 uv_mutex_unlock(&mutex);
}
uv_queue_work — 针对cpu密集型提交一个任务
  • 通过 uv_queue_work 提交的任务,是对应一个 request 的。如果该 request 对应的任务没有执行完,则事件循环不会退出。而通过 uv__work_submit 方式提交的任务就算没有执行完,也不会影响事件循环的退出。
  • uv_work_t
struct uv_work_t {
 UV_REQ_FIELDS
 uv_loop_t* loop;
 uv_work_cb work_cb;
 uv_after_work_cb after_work_cb;
 UV_WORK_PRIVATE_FIELDS
};
  • 源码
    int uv_queue_work(uv_loop_t* loop,
                      uv_work_t* req,
                      uv_work_cb work_cb,
                      uv_after_work_cb after_work_cb) {
      if (work_cb == NULL)
        return UV_EINVAL;
      
      // 使 (loop)->active_reqs.count++
      uv__req_init(loop, req, UV_WORK);
      
      req->loop = loop;
      req->work_cb = work_cb;
      req->after_work_cb = after_work_cb;
      
      uv__work_submit(loop,
                      &req->work_req,
                      UV__WORK_CPU,      // 是CPU密集型的
                      uv__queue_work,    // 当这个任务被执行的时候。他会执行函数 uv__queue_work
                      uv__queue_done);   // 当这个任务执行结束的时候。他会执行函数 uv__queue_done
      return 0;
    }
    
    
    static void uv__queue_work(struct uv__work* w) {
      // 通过结构体某字段拿到结构体地址
      uv_work_t* req = container_of(w, uv_work_t, work_req);
    
      req->work_cb(req);
    }
    
    
    static void uv__queue_done(struct uv__work* w, int err) {
      uv_work_t* req;
    
      req = container_of(w, uv_work_t, work_req);
      // 使 (loop)->active_reqs.count--
      uv__req_unregister(req->loop, req);
    
      if (req->after_work_cb == NULL)
        return;
    
      req->after_work_cb(req, err);
    }
    
worker —— 线程池中的线程执行的函数
  • 线程池中把任务分为三种。并且对于慢 io 类型的 任务,还限制了线程数。其余的逻辑和一般的线程池类似,就是互斥访问任务队列,然后取出节点执行,最后执行回调。不过 libuv 这里不是直接回调用户的函数。而是通知主进程。由主进程处理
  • 源码
     // 该线程池在用户提交了第一个任务的时候初始化,而不是系统启动的时候就初始化
    static void worker(void* arg) {
      struct uv__work* w;
      QUEUE* q;
      int is_slow_work;
    
      // 线程启动成功,因为初始化线程的时候,等待所有线程都执行成功之后才会往下执行
      uv_sem_post((uv_sem_t*) arg);
      arg = NULL;
    
      // 加锁互斥访问任务队列
      uv_mutex_lock(&mutex);
    
      for (;;) {
        /*
            1 队列为空,
            2 队列不为空,但是队列里只有慢 IO 任务且正在执行的慢 IO 任务个数达到阈值
            则空闲线程加一,防止慢 IO 占用过多线程,导致其他快的任务无法得到执行
        */
        while (QUEUE_EMPTY(&wq) ||
               (QUEUE_HEAD(&wq) == &run_slow_work_message &&
                QUEUE_NEXT(&run_slow_work_message) == &wq &&
                slow_io_work_running >= slow_work_thread_threshold())) {
          idle_threads += 1;
          // 阻塞,等待队列中有任务的时候唤醒
          uv_cond_wait(&cond, &mutex);
          // 被唤醒,开始干活,空闲线程数减一
          idle_threads -= 1;
        }
    
        // 取出头结点,头指点可能是退出消息、慢 IO,一般请求
        q = QUEUE_HEAD(&wq);
    
        // 如果头结点是退出消息,则结束线程
        if (q == &exit_message) {
          // 唤醒其他因为没有任务正阻塞等待任务的线程,别的线程同样取出这个节点,结束线程...
          // 最后线程会全部结束
          uv_cond_signal(&cond);
          uv_mutex_unlock(&mutex);
          break;
        }
    
        // 移除节点
        QUEUE_REMOVE(q);
        // 重置前后指针
        QUEUE_INIT(q);  /* Signal uv_cancel() that the work req is executing. */
    
        is_slow_work = 0;
    
        /*
          如果当前节点等于慢 IO 节点,上面的 while 只判断了是不是只有慢 io 任务且达到
          阈值,这里是任务队列里肯定有非慢 io 任务,可能有慢 io,如果有慢 io 并且正在
          执行的个数达到阈值,则先不处理该慢 io 任务,继续判断是否还有非慢 io 任务可
          执行。
        */
        if (q == &run_slow_work_message) {
          // 遇到阈值,重新入队
          if (slow_io_work_running >= slow_work_thread_threshold()) {
            QUEUE_INSERT_TAIL(&wq, q);
            continue;
          }
    
          // 没有慢 IO 任务则继续
          if (QUEUE_EMPTY(&slow_io_pending_wq))
            continue;
    
          // 有慢 io,开始处理慢 IO 任务
          is_slow_work = 1;
          // 正在处理慢 IO 任务的个数累加,用于其他线程判断慢 IO 任务个数是否达到阈值
          slow_io_work_running++;
    
          // 摘下一个慢 io 任务
          q = QUEUE_HEAD(&slow_io_pending_wq);
          QUEUE_REMOVE(q);
          QUEUE_INIT(q);
    
          /*
            取出一个任务后,如果还有慢 IO 任务则把慢 IO 标记节点重新入队,
            表示还有慢 IO 任务,因为上面把该标记节点出队了
          */
          if (!QUEUE_EMPTY(&slow_io_pending_wq)) {
            // 有空闲线程则唤醒他,因为还有任务处理
            QUEUE_INSERT_TAIL(&wq, &run_slow_work_message);
            if (idle_threads > 0)
              uv_cond_signal(&cond);
          }
        }
    
        // 不需要操作队列了,尽快释放锁
        uv_mutex_unlock(&mutex);
    
        // q 是慢 IO 或者一般任务
        w = QUEUE_DATA(q, struct uv__work, wq);
        // 执行业务的任务函数,该函数一般会阻塞
        w->work(w);
    
        // 准备操作 loop 的任务完成队列,加锁
        uv_mutex_lock(&w->loop->wq_mutex);
        // 置空说明指向完了,不能被取消了,见 cancel 逻辑
        w->work = NULL;  
    
        // 执行完任务,插入到 loop 的 wq 队列,在 uv__work_done 的时候会执行队列中节点的 done 函数
        QUEUE_INSERT_TAIL(&w->loop->wq, &w->wq);
        // 通知 loop 的 wq_async 节点
        uv_async_send(&w->loop->wq_async);
        uv_mutex_unlock(&w->loop->wq_mutex);
    
        // 为下一轮操作任务队列加锁
        uv_mutex_lock(&mutex);
        if (is_slow_work) {
          // 执行完慢 IO 任务,记录正在执行的慢 IO 个数变量减 1,上面加锁保证了互斥访问这个变量
          slow_io_work_running--;
        }
      }
    }
    
主进程初始化线程池的过程
  • uv_loop_init
    uv_async_init(loop, &loop->wq_async, uv__work_done);
    
  • 线程池中的线程执行的函数 work 最后有一句
    uv_async_send(&w->loop->wq_async);
    
  • wq_async 是 用于线程池和主线程通信的 async handle 。 他对应 的回调是 uv__work_done 。所以当 一 个 线 程 池 的 线 程 任 务 完 成 时 , 通 过 uv_async_send(&w->loop->wq_async)设置 loop->wq_async.pending = 1,然后 通知 io 观察者。Libuv 在 poll io 阶段就会执行该 handle 对应的回调。该 io 观察者的 回调是 uv__work_done 函数
uv__work_done
  • 源码
    void uv__work_done(uv_async_t* handle) {
      struct uv__work* w;
      uv_loop_t* loop;
      QUEUE* q;
      QUEUE wq;
      int err;
    
      // 通过结构体字段获得结构体首地址
      loop = container_of(handle, uv_loop_t, wq_async);
      // 准备处理队列,加锁
      uv_mutex_lock(&loop->wq_mutex);
      // 把 loop->wq 队列的节点全部移到 wp 变量中,这样一来可以尽快释放锁
      QUEUE_MOVE(&loop->wq, &wq);
      // 不需要使用了,解锁
      uv_mutex_unlock(&loop->wq_mutex);
    
      // wq 队列的节点来源是在线程的 worker 里插入
      while (!QUEUE_EMPTY(&wq)) {
        q = QUEUE_HEAD(&wq);
        QUEUE_REMOVE(q);
    
        w = container_of(q, struct uv__work, wq);
        err = (w->work == uv__cancelled) ? UV_ECANCELED : 0;
        // 执行回调
        w->done(w, err);
      }
    }
    
uv__work_cancel 取消提交的任务
  • 源码
    static int uv__work_cancel(uv_loop_t* loop, uv_req_t* req, struct uv__work* w) {
      int cancelled;
    
      // 加锁,为了把节点移出队列
      uv_mutex_lock(&mutex);
      // 加锁,为了判断 w->wq 是否为空
      uv_mutex_lock(&w->loop->wq_mutex);
    
      /*
      w 在任务队列中并且任务函数 work 不为空,则可取消,
      在 work 函数中,如果执行完了任务,会把 work 置 NULL,
      所以一个任务可以取消的前提是他还没执行完。或者说还没执行过
      */
      cancelled = !QUEUE_EMPTY(&w->wq) && w->work != NULL;
      // 从任务队列中删除该节点
      if (cancelled)
        QUEUE_REMOVE(&w->wq);
    
      uv_mutex_unlock(&w->loop->wq_mutex);
      uv_mutex_unlock(&mutex);
    
      // 不能取消
      if (!cancelled)
        return UV_EBUSY;
    
      // 重置回调函数
      w->work = uv__cancelled;
      uv_mutex_lock(&loop->wq_mutex);
      /*
        插入 loop 的 wq 队列,对于取消的动作,libuv 认为是任务执行完了。
        所以插入已完成的队列,不过他的回调是 uv__cancelled 函数,
        而不是用户设置的回调
      */
      QUEUE_INSERT_TAIL(&loop->wq, &w->wq);
      // 通知主线程有任务完成
      uv_async_send(&loop->wq_async);
      uv_mutex_unlock(&loop->wq_mutex);
    
      return 0;
    }
    
  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值