Nodejs事件引擎libuv源码剖析之:高效线程池(threadpool)的实现
声明:本文为原创博文,转载请注明出处。
Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们。在网络编程中,一般都是基于Reactor线程模型的变种,无论其怎么演化,其核心组件都包含了Reactor实例(提供事件注册、注销、通知功能)、多路复用器(由操作系统提供,比如kqueue、select、epoll等)、事件处理器(负责事件的处理)以及事件源(linux中这就是描述符)这四个组件。一般,会单独启动一个线程运行Reactor实例来实现真正的异步操作。但是,依赖操作系统提供的系统调用来实现异步是有局限的,比如在Reactor模型中我们只能监听到:网络IO事件、signel(信号)、超时事件以及一些管道事件等,但这些事件也只是通知我们资源可读或者可写,真正的读写操作(read和write)还是同步的(也就是你必须等到read或者write返回,虽然linux提供了aio,但是其有诸多槽点),那么Nodejs的全异步是如何做到的呢?你可能会很快想到,就是启用单独的线程来做同步的事情,这也是libuv的设计思路,借用官网的一张图,说明一切:

由上图可以看到,libuv实现了一套自己的线程池来处理所有同步操作(从而模拟出异步的效果),下面就来看一下该线程池的具体实现吧!
一、线程池模型
说道线程池,在java领域中,jdk本身就提供了多种线程池实现,几乎所有的线程池都遵循以下模型(任务队列+线程池):

libuv自身定义了一个非常精炼、高效的队列(双向循环链表),只用了几个简单的宏定义将其实现,具体实现方式可以参见我的另一篇博文:libuv高效队列的实现。现在队列有了,来看一下task的定义:
struct uv__work {
void (*work)(struct uv__work *w);
void (*done)(struct uv__work *w, int status);
struct uv_loop_s* loop;
void* wq[];
};
uv__work就代表一个task,可以看到里面有两个函数指针(work代表任务实际操作,done用于对任务进行状态确认)。wq成员就是一个QUEUE的节点, uv__work就是通过wq与其他 uv__work连接成一个队列。
下面来看一下threadpool的初始化,代码如下:
#define MAX_THREADPOOL_SIZE 128 static uv_once_t once = UV_ONCE_INIT;
static uv_cond_t cond;
static uv_mutex_t mutex;
static unsigned int idle_threads;//当前空闲的线程数
static unsigned int nthreads;
static uv_thread_t* threads;
static uv_thread_t default_threads[];
static QUEUE exit_message;
static QUEUE wq;//线程池全部会检查这个queue,一旦发现有任务就执行,但是只能有一个线程抢占到
static volatile int initialized; static void init_once(void) {
unsigned int i;
const char* val;
// 线程池中的线程数,默认值为4
nthreads = ARRAY_SIZE(default_threads);
val = getenv("UV_THREADPOOL_SIZE");
if (val != NULL)
nthreads = atoi(val);
if (nthreads == )
nthreads = ;
if (nthreads > MAX_THREADPOOL_SIZE)
nthreads = MAX_THREADPOOL_SIZE; threads = default_threads;
if (nthreads > ARRAY_SIZE(default_threads)) {
// 分配线程句柄
threads = uv__malloc(nthreads * sizeof(threads[]));
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); // 创建nthreads个线程
for (i = ; i < nthreads; i++)
if (uv_thread_create(threads + i, worker, NULL))
abort(); initialized = ;
}
上面的代码中,一共创建了nthreads个线程,那么每个线程的执行代码是什么呢?由线程创建代码:uv_thread_create(threads + i, worker, NULL),可以看到,每一个线程都是执行worker函数,下面看看worker函数都在做什么:
/* To avoid deadlock with uv_cancel() it's crucial that the worker
* never holds the global mutex and the loop-local mutex at the same time.
*/
static void worker(void* arg) {
struct uv__work* w;
QUEUE* q; (void) arg; for (;;) {
// 因为是多线程访问,因此需要加锁同步
uv_mutex_lock(&mutex); // 如果任务队列是空的
while (QUEUE_EMPTY(&wq)) {
// 空闲线程数加1
idle_threads += ;
// 等待条件变量
uv_cond_wait(&cond, &mutex);
// 被唤醒之后,说明有任务被post到队列,因此空闲线程数需要减1
idle_threads -= ;
} // 取出队列的头部节点(第一个task)
q = QUEUE_HEAD(&wq); if (q == &exit_message)
uv_cond_signal(&cond);
else {
// 从队列中移除这个task
QUEUE_REMOVE(q);
QUEUE_INIT(q); /* Signal uv_cancel() that the work req is
executing. */
} uv_mutex_unlock(&mutex); if (q == &exit_message)
break; // 取出uv__work首地址
w = QUEUE_DATA(q, struct uv__work, wq);
// 调用task的work,执行任务
w->work(w); uv_mutex_lock(&w->loop->wq_mutex);
w->work = NULL; /* Signal uv_cancel() that the work req is done
executing. */
QUEUE_INSERT_TAIL(&w->loop->wq, &w->wq);
uv_async_send(&w->loop->wq_async);
uv_mutex_unlock(&w->loop->wq_mutex);
}
}
可以看到,多个线程都会在worker方法中等待在conn条件变量上,一旦有任务加入队列,线程就会被唤醒,然后只有一个线程会得到任务的执行权,其他的线程只能继续等待。
那么如何向队列提交一个task呢?看以下代码:
void uv__work_submit(uv_loop_t* loop,
struct uv__work* w,
void (*work)(struct uv__work* w),
void (*done)(struct uv__work* w, int status)) {
uv_once(&once, init_once);
// 构造一个task
w->loop = loop;
w->work = work;
w->done = done;
// 将其插入任务队列
post(&w->wq);
}
接着看post做了什么:
static void post(QUEUE* q) {
// 同步队列操作
uv_mutex_lock(&mutex);
// 将task插入队列尾部
QUEUE_INSERT_TAIL(&wq, q);
// 如果当前有空闲线程,就向条件变量发送信号
if (idle_threads > )
uv_cond_signal(&cond);
uv_mutex_unlock(&mutex);
}
有提交任务,就肯定会有取消一个任务的操作,是的,他就是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);
uv_mutex_lock(&w->loop->wq_mutex);
// 只有当前队列不为空并且要取消的uv__work有效时才会继续执行
cancelled = !QUEUE_EMPTY(&w->wq) && w->work != NULL;
if (cancelled)
QUEUE_REMOVE(&w->wq);// 从队列中移除task
uv_mutex_unlock(&w->loop->wq_mutex);
uv_mutex_unlock(&mutex);
if (!cancelled)
return UV_EBUSY;
// 更新这个task的状态
w->work = uv__cancelled;
uv_mutex_lock(&loop->wq_mutex);
QUEUE_INSERT_TAIL(&loop->wq, &w->wq);
uv_async_send(&loop->wq_async);
uv_mutex_unlock(&loop->wq_mutex);
return ;
}
至此,一个线程池的组成以及实现原理都说完了,可以看到,libuv几乎是用了最少的代码完成了高效的线程池,这对于我们平时写代码时具有很好的借鉴意义,文中涉及到uv_req_t以及uv_loop_t等结构我都直接跳过,因为这牵扯到libuv的其他组件,我将在以后的源码剖析中逐步阐述,谢谢你能看到这里。
Nodejs事件引擎libuv源码剖析之:高效线程池(threadpool)的实现的更多相关文章
- Nodejs事件引擎libuv源码剖析之:句柄(handle)结构的设计剖析
声明:本文为原创博文,转载请注明出处. 句柄(handle)代表一种对持有资源的索引,句柄的叫法在window上较多,在unix/linux等系统上大多称之为描述符,为了抽象不同平台的差异,libuv ...
- Nodejs事件引擎libuv源码剖析之:请求(request)结构的设计剖析
声明:本文为原创博文,转载请注明出处. 在libuv中,请求(request)代表一个用户向libuv发出的指令,比如uv_connect_s就表示一个tcp的连接请求.uv_work ...
- Nodejs事件引擎libuv源码剖析之:高效队列(queue)的实现
声明:本文为原创博文,转载请注明出处. 在libuv中,有一个只使用简单的宏封装成的高效队列(queue),现在我们就来看一下它是怎么实现的. 首先,看一下queue中最基本的几个宏: typede ...
- 《java.util.concurrent 包源码阅读》13 线程池系列之ThreadPoolExecutor 第三部分
这一部分来说说线程池如何进行状态控制,即线程池的开启和关闭. 先来说说线程池的开启,这部分来看ThreadPoolExecutor构造方法: public ThreadPoolExecutor(int ...
- 硬核干货:4W字从源码上分析JUC线程池ThreadPoolExecutor的实现原理
前提 很早之前就打算看一次JUC线程池ThreadPoolExecutor的源码实现,由于近段时间比较忙,一直没有时间整理出源码分析的文章.之前在分析扩展线程池实现可回调的Future时候曾经提到并发 ...
- 从源码角度来分析线程池-ThreadPoolExecutor实现原理
作为一名Java开发工程师,想必性能问题是不可避免的.通常,在遇到性能瓶颈时第一时间肯定会想到利用缓存来解决问题,然而缓存虽好用,但也并非万能,某些场景依然无法覆盖.比如:需要实时.多次调用第三方AP ...
- Java并发包源码学习系列:线程池ThreadPoolExecutor源码解析
目录 ThreadPoolExecutor概述 线程池解决的优点 线程池处理流程 创建线程池 重要常量及字段 线程池的五种状态及转换 ThreadPoolExecutor构造参数及参数意义 Work类 ...
- Java并发包源码学习系列:线程池ScheduledThreadPoolExecutor源码解析
目录 ScheduledThreadPoolExecutor概述 类图结构 ScheduledExecutorService ScheduledFutureTask FutureTask schedu ...
- JUC源码学习笔记5——线程池,FutureTask,Executor框架源码解析
JUC源码学习笔记5--线程池,FutureTask,Executor框架源码解析 源码基于JDK8 参考了美团技术博客 https://tech.meituan.com/2020/04/02/jav ...
随机推荐
- 多War项目中静态文件的共享方案
[原创申明:文章为原创,欢迎非盈利性转载,但转载必须注明来源] 在互联网产品中,一般会有多个项目(Jar.WAR)组成一个产品线.这些WAR项目,因为使用相同的前端架构(jQuery.easyui等) ...
- Java使用POS打印机(无驱)
使用原因:应项目要求,需要使用打印机,但是如果使用Windows驱动来实现打印,在某些条件下会发生网络堵塞等,而且没有提示,所以为了确保信息的完整,避免数据丢失.我们使用无驱打印(直接写端口的方法), ...
- 2.快速部署MySQL主从复制
1.快速部署MySQL主从复制 [root@mysql ~]# mysql -uroot -p123456 -S /data/3307/mysql.sock -e "show slave ...
- 【C++】多态性(函数重载与虚函数)
多态性就是同一符号或名字在不同情况下具有不同解释的现象.多态性有两种表现形式: 编译时多态性:同一对象收到相同的消息却产生不同的函数调用,一般通过函数重载来实现,在编译时就实现了绑定,属于静态绑定. ...
- GDB 完全教程
一.简介 GDB是一个功能强大的调试器,它是一个自由软件,能够用在许多UNIX平台上.它同时也是Linux系统中的默认调试器.GDB已被移植到许多其他的计算机平台上,并且能够用于调试嵌入式实时系统.一 ...
- CSS3 基于关系的选择器
常见的基于关系的选择器 选择器 选择的元素 A E 元素A的任一后代元素E (后代节点指A的子节点,子节点的子节点,以此类推) A > E 元素A的任一子元素E(也就是直系后代) E:first ...
- phoneGap+cordova+ionic混合app开发环境搭建
参考链接:http://www.w2 bc.com/article/177257 待补充
- [LeetCode] Binary Tree Right Side View 二叉树的右侧视图
Given a binary tree, imagine yourself standing on the right side of it, return the values of the nod ...
- selector 使用说明
android:state_pressed=["true" | "false"]//是否触摸 android:state_focused=["true ...
- 如何获取用户的真实IP
在JSP里,获取客户端的IP地址的方法是:request.getRemoteAddr(),这种方法在大部分情况下都是有效的.但是在通过了 Apache,Nagix等反向代理软件就不能获取到客户端的真实 ...