好像博客有观众,那每一篇都画个图吧!

  本节简图如下。

  上一篇其实啥也没讲,不过node本身就是这么复杂,走流程就要走全套。就像曾经看webpack源码,读了300行代码最后就为了取package.json里面的main属性,导致我直接弃坑了,垃圾源码看完对脑子没一点好处。回头看了我之前那篇博客,同步那块讲的还像回事,异步就惨不忍睹了。不过讲道理,异步中涉及锁、底层操作系统API(iocp)的部分我到现在也不太懂,毕竟没有实际的多线程开发经验,只是纯粹的技术爱好者。

  这一篇再次进入libuv内部,从uv_fs_stat开始,操作系统以windows为准,方法源码如下。

// 参数分别为事件轮询对象loop、管理事件处理的对象req、路径path、事件回调cb
int uv_fs_stat(uv_loop_t* loop, uv_fs_t* req, const char* path, uv_fs_cb cb) {
int err; INIT(UV_FS_STAT);
err = fs__capture_path(req, path, NULL, cb != NULL);
if (err) {
return uv_translate_sys_error(err);
} POST;
}

  其实Unix版本的代码更简洁,直接就是

int uv_fs_stat(uv_loop_t* loop, uv_fs_t* req, const char* path, uv_fs_cb cb) {
INIT(STAT);
PATH;
POST;
}

  问题不大,都是三步。

  前面两步在那篇都有介绍,这里就不重复了。大概就是根据操作类型初始化req对象,然后处理一下路径,分配合理的空间给path字符串这些。

  重点还是放在POST宏。

#define POST                                                                  \
do { \
if (cb != NULL) { \
uv__req_register(loop, req); \
// word_req是一个类型为uv__work的结构体
// UV__WORK_FAST_IO是I/O操作类型
// uv__fs_work是一个函数
// uv__fs_done也是一个函数
uv__work_submit(loop, \
&req->work_req, \
UV__WORK_FAST_IO, \
uv__fs_work, \
uv__fs_done); \
return ; \
} else { \
uv__fs_work(&req->work_req); \
return req->result; \
} \
} \
while ()

  由于只关注异步操作,所以看if分支。参数已经在注释中给出,还需要注意的一个点是方法名,register、submit,即注册、提交。意思是,异步操作中,在这里也不是执行I/O的地点,实际上还有更深入的地方,继续往后面看。

  uv__req_register这个就不看了,简单讲是把loop的active_handle++,每一轮轮询结束后会检测当前loop是否还有活跃的handle需要处理,有就会继续跑,判断标准就是active_handle数量是否大于0。

  直接看下一步uv__work_submit。

// uv__word结构体
struct uv__work {
void (*work)(struct uv__work *w);
void (*done)(struct uv__work *w, int status);
struct uv_loop_s* loop;
void* wq[];
}; // 参数参考上面 init_once是一个方法
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(&w->wq, kind);
}

  又是两部曲,第一个uv_once如其名,这个方法只会执行一次,然后将loop对象和两个方法挂在前面req的uv__work结构体上,最后调用post。

  uv_once这个方法有点意思,本身跟stat操作本身毫无关系,只是对所有I/O操作做一个准备工作,所有的I/O操作都会预先调一下这个方法。windows、Unix系统的处理方式完全不同,这里贴一贴代码,Unix不想看也看不懂,搞搞windows系统的。

void uv_once(uv_once_t* guard, void (*callback)(void)) {
// 调用过方法此处ran为1 直接返回
if (guard->ran) {
return;
} uv__once_inner(guard, callback);
} static void uv__once_inner(uv_once_t* guard, void (*callback)(void)) {
DWORD result;
HANDLE existing_event, created_event; // 创建或打开命名或未命名的事件对象
created_event = CreateEvent(NULL, , , NULL);
if (created_event == ) {
uv_fatal_error(GetLastError(), "CreateEvent");
}
// 对&guard->event与NULL做原子比较 如果相等则将created_event赋予&guard->event
// 返回第一个参数的初始值
existing_event = InterlockedCompareExchangePointer(&guard->event,
created_event,
NULL); // 如果第一个参数初始值为NULL 说明该线程抢到了方法第一次执行权利
if (existing_event == NULL) {
/* We won the race */
callback(); result = SetEvent(created_event);
assert(result);
guard->ran = ;
} else {
// ...
}
}

  分块来解释一下上面的函数吧。

  • libuv这里直接跟操作系统通信,在windows上需要借助其自身的event模块来辅助异步操作。
  • 提前剧透一下,所有的I/O操作是调用独立线程进行处理,所以这个uv_once会被多次调用,而多线程抢调用的时候有两种情况;第一种最简单,第一名已经跑完所有流程,将ran设置为1,其余线程直接被挡在了uv_once那里直接返回了。第二种就较为复杂,两个线程同时接到了这个任务,然后都跑进了uv_once_inner中去了,如何保证参数callback只会被调用一次?这里用上了windows内置的原子指针比较方法InterlockedCompareExchangePointer。何谓原子比较?这是只有在多线程才会出现的概念,原子性保证了每次读取变量的值都是根据最新信息计算出来的,避免了多线程经常出现的竞态问题,详细文献可以参考wiki。
  • 只有第一个抢到了调用权利的线程才会进入if分支,之后调用callback方法,并设置event,那个SetEvent也是windowsAPI,有兴趣自己研究去。

  最后,所有的代码流向都为了执行callback,参数表明这是一个函数指针,无返回值无参数,叫init_once。

static void init_once(void) {
#ifndef _WIN32
// 用32位系统的去买新电脑
// 略...
#endif
init_threads();
}

  有意思咯,线程来了。

  先表明,libuv中有一个非常关键的数据结构:队列,在src/queue.h。很多地方(比如之前讲轮询的某一阶段取对应的callback时)我虽然说的是链表,但实际上用的是这个,由于链表是队列的超集,而且比较容易理解,总的来说也不算错。说这么多,其实是初始化线程池会用到很多queue的宏,我不想讲,后面会单独开一篇说。

  下面上代码。

static void init_threads(void) {
unsigned int i;
const char* val;
uv_sem_t sem; // 线程池默认大小为4
nthreads = ARRAY_SIZE(default_threads);
// 可以通过环境变量UV_THREADPOOL_SIZE来手动设置
val = getenv("UV_THREADPOOL_SIZE");
// 如果设成0会变成1 大于上限会变成128
if (val != NULL)
nthreads = atoi(val);
if (nthreads == )
nthreads = ;
if (nthreads > MAX_THREADPOOL_SIZE)
nthreads = MAX_THREADPOOL_SIZE; threads = default_threads;
// 分配空间 静态变量threads负责管理线程
if (nthreads > ARRAY_SIZE(default_threads)) {
threads = uv__malloc(nthreads * sizeof(threads[]));
if (threads == NULL) {
nthreads = ARRAY_SIZE(default_threads);
threads = default_threads;
}
} // 这里是锁和QUEUE相关... // 这里给线程设置任务 唤醒后直接执行worker方法
for (i = ; i < nthreads; i++)
if (uv_thread_create(threads + i, worker, &sem))
abort(); // 无关代码...
}

  除去一些不关心的代码,剩下的就是判断是否有手动设置线程池数量,然后初始化分配空间,最后循环给每一个线程分配任务。

  这个worker可以先简单看一下,大部分内容都是QUEUE相关,详细内容全部写在注释里面。

static void worker(void* arg) {
// ... // 这个是给代码块加锁 很多地方都有
uv_mutex_lock(&mutex);
for (;;) {
// ..。 // 从队列取出一个节点
q = QUEUE_HEAD(&wq);
// 表示没有更多要处理的信息 直接退出绝不能继续走下面的
// 退出前还会两个操作 1.唤醒另一个线程再次处理这个方法(可能下一瞬间来活了) 2.去掉锁
if (q == &exit_message) {
uv_cond_signal(&cond);
uv_mutex_unlock(&mutex);
break;
} // 从队列中移除这个节点
QUEUE_REMOVE(q);
QUEUE_INIT(q); is_slow_work = ;
// node过来的都是快速通道 不会走这里
if (q == &run_slow_work_message) {
//...
} // 由于已经从队列中移除了对应节点 这里可以把锁去掉了
uv_mutex_unlock(&mutex); // 从节点取出对应的任务 执行work也就是实际的I/O操作(比如fs.stat...) 参考上面的uv__work_submit方法
w = QUEUE_DATA(q, struct uv__work, wq);
w->work(w); // 这里也需要加锁 执行完节点任务后需要将结果添加到word_queue的队列中
uv_mutex_lock(&w->loop->wq_mutex);
w->work = NULL;
QUEUE_INSERT_TAIL(&w->loop->wq, &w->wq);
uv_async_send(&w->loop->wq_async);
uv_mutex_unlock(&w->loop->wq_mutex); // 由于是for(;;) 这里加锁纯粹是为了下一次提前准备循环
uv_mutex_lock(&mutex);
if (is_slow_work) {
/* `slow_io_work_running` is protected by `mutex`. */
slow_io_work_running--;
}
}
}

  注意是静态方法,所以也需要处理多线程问题。注释我写的非常详细了,可以慢慢看,不懂C++也大概能明白流程。

  还以为这一篇能搞完,没想到这个流程有点长,先这样吧。

浅析libuv源码-node事件轮询解析(3)的更多相关文章

  1. 浅析libuv源码-node事件轮询解析(2)

    上一篇讲了轮询的边角料,这篇进入正题.(竟然真有人看我博客,上两个图给你们整理下思路) 这是轮询总流程图. 下图为本节内容简图. Poll for I/O The loop blocks for I/ ...

  2. 浅析libuv源码-node事件轮询解析(1)

    好久没写东西了,过了一段咸鱼生活,无意中想起了脉脉上面一句话: 始终保持自己的竞争力.所以,继续开写! 一般的JavaScript源码看的已经没啥意思了,我也不会写什么xx入门新手教程,最终决定还是啃 ...

  3. 浅析libuv源码-node事件轮询解析(4)

    这篇应该能结,简图如下. 上一篇讲到了uv__work_submit方法,接着写了. void uv__work_submit(uv_loop_t* loop, struct uv__work* w, ...

  4. 浅析libuv源码-编译启动

    面试的间隙回头复习了一下node,感觉node就像一个胶带,把V8和libuv粘在了一起. V8毫无疑问,负责解析执行JavaScript,相当于语言层面的桥梁:而libuv则是负责操作系统底层功能的 ...

  5. 浅析libuv源码-获取精确时间

    在Timer模块中有提到,libuv控制着延迟事件的触发,那么必须想办法精确控制时间. 如果是JS,获取当前时间可以直接通过Date.now()得到一个时间戳,然后将两段时间戳相减得到时间差.一般情况 ...

  6. node.js事件轮询(1)

    事件轮询(引用) 事件轮询是node的核心内容.一个系统(或者说一个程序)中必须至少包含一个大的循环结构(我称之为"泵"),它是维持系统持续运行的前提.nodejs中一样包含这样的 ...

  7. 理解Node.js的事件轮询

    前言 总括 : 原文地址:理解Node.js的事件轮询 Node小应用:Node-sample 智者阅读群书,亦阅历人生 正文 Node.js的两个基本概念 Node.js的第一个基本概念就是I/O操 ...

  8. 对Node.JS的事件轮询(Event Loop)的理解

    title: Node.JS的事件轮询(event loop)的理解 categories: 理解 tags: Node JS 机制 当我们知道I/O操作和创建新线程的开销是巨大的! 网站延迟的开销 ...

  9. Node.js的异步IO和事件轮询

     想象一下,以前我们在写程序时, 如果程序在I/O上阻塞了,当有更多请求过来时,服务器会怎么处理呢?在这种情景中通常会用多线程的方式.一种常见的实现是给每个连接分配一个线程,并为那些连接设置一个线程池 ...

随机推荐

  1. linux 进程学习笔记-进程信号sigal

    信号(或软中断)是在软件层次上对中断的一个模拟,其运行在“用户空间”,一个进程对另外一个或几个进程通过发送信号来实现异步通信.当接收进程接收到信号后,其可以注册一下处理函数来说对这些信号进行处理(也可 ...

  2. 解决系统存在大量TIME_WAIT状态的连接

    系统存在大量TIME_WAIT状态的连接,通过调整内核参数解决, vi /etc/sysctl.conf 编辑文件,加入以下内容:net.ipv4.tcp_syncookies = 1net.ipv4 ...

  3. AtCoder AGC #4 Virtual Participation

    我好懒啊QAQ 老规矩 从C开始 C.给一个矩阵,里面有一些紫色方块,你需要涂两个矩阵,一个红色,一个蓝色,保证你涂的颜色四连通 然后把红色蓝色矩阵叠起来要求紫色的地方必须是紫色,其他地方不能是紫色 ...

  4. ACM学习历程——HDU4472 Count(数学递推) (12年长春区域赛)

    Description Prof. Tigris is the head of an archaeological team who is currently in charge of an exca ...

  5. 重学JAVA基础(二):Java反射

        看一下百度的解释:       JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性:这种动态获取的信息     ...

  6. iOS :undefined symbols for architecture x86_64

    转自:http://www.th7.cn/Program/IOS/201408/268371.shtml 问题描述:为了适配iPhone 5s的64位处理器,在编译选项中加入了arm64架构.但是发现 ...

  7. 表单enctype属性

    首先知道enctype这个属性管理的是表单的MIME编码.共有三个值可选:1.application/x-www-form-urlencoded2.multipart/form-data3.text/ ...

  8. Teams Formation

    题意: 给定一长度为 n 的整数序列 $a$,将其复制m次,并接成一条链,每相邻K个相同的整数会消除,然后其他的整数继续结成一条链,直到不能消除为止,求问最终剩余多少个整数. 解法: 首先将长度为n的 ...

  9. POJ - 3268 Silver Cow Party SPFA+SLF优化 单源起点终点最短路

    Silver Cow Party One cow from each of N farms (1 ≤ N ≤ 1000) conveniently numbered 1..N is going to ...

  10. MySQL8.0.16新特性:The Communication Protocol In Group Replication

    MGR优雅升级到MySQL8.0.16 传统的升级手段之一,5.7 MGR集群与8.0 MGR集群进行数据传输,程序切换新集群后测试是否正常. 如果不正常,要么将新集群的新增数据同步回旧集群,要么就舍 ...