事件循环系统的设计和实现

事件循环相信大家都不陌生,很多同学都知道事件循环是一个"死循环",今天我们看一下这个死循环到底是怎样的。我们先看一个朴素版的事件循环系统。

class EventSystem {
  constructor() {
    // 任务队列
    this.queue = [];
  }// 追加任务
  enQueue(func) {
    this.queue.push(func);
  }// 事件循环
  run() {
    while(1) {
      while(this.queue.length) {
         const func = this.queue.shift();
         func();
       }
    }
  }
}
// 新建一个事件循环系统
const eventSystem = new EventSystem();
// 生产任务
eventSystem.enQueue(() => {
  console.log('hi');
});
// 启动事件循环
eventSystem.run();

以上代码实现了一个非常朴素的事件循环系统

1 新建一个事件循环系统
2 生产任务
3 启动事件循环系统
但是我们发现当没有任务的时候,事件循环系统陷入了死循环,这无疑浪费了cpu。我们看一下执行以上代码的cpu的情况(我电脑4核,可以看到以上代码对应的进程几乎完全占据了一个cpu,1/4)。

接着我们优化一下这个朴素版的事件循环。

class EventSystem {
  constructor() {
    // 任务队列
    this.queue = [];
    // 是否需要停止任务队列
    this.stop = 0;
    // 超时处理
    this.timeoutResolve = null;
  }
  // 没有任务时,事件循环的睡眠时间
  sleep(time) {
    return new Promise((resolve) => {
      let timer = null;
      // 记录resolve,可能在睡眠期间有任务到来,则需要提前唤醒
      this.timeoutResolve = () => {
        clearTimeout(timer);
        timer = null;
        this.timeoutResolve = null;
        resolve();
      };
      timer = setTimeout(() => {
        if (timer) {
           console.log('timeout');
          this.timeoutResolve = null;
          resolve();
        }
      }, time);
    });
  }
  // 停止事件循环
  setStop() {
    this.stop = 1;
    this.timeoutResolve && this.timeoutResolve();
  }// 追加任务
  enQueue(func) {
    this.queue.push(func);
    this.timeoutResolve && this.timeoutResolve();
  }// 事件循环
  async run() {
    while(1 && this.stop === 0) {
      while(this.queue.length) {
         const func = this.queue.shift();
         func();
       }
       // 没有任务了,一直等待(Math.pow(2, 31) - 1为nodejs中定时器的最大值)
       await this.sleep(Math.pow(2, 31) - 1);
    }
  }
}
// 新建一个事件循环系统
const eventSystem = new EventSystem();
// 生产任务
eventSystem.enQueue(() => {
  console.log('hi');
});
// 模拟定时生成一个任务
setTimeout(() => {
  eventSystem.enQueue(() => {
    console.log('hello');
  });
}, 1000);
// 模拟退出事件循环
setTimeout(() => {
  eventSystem.setStop();
}, 2000);
// 启动事件循环
eventSystem.run();

上面代码的执行结果如下

1 启动事件循环时输出hi。
2 事件循环进入睡眠,1s时被唤醒,输出hello。
3 2s后退出事件循环。
麻雀虽小五脏俱全,以上代码虽然只是个demo,但是已经具备了事件循环的一些核心概念。

1 事件循环的整体架构是一个while循环

2 定义任务类型和队列,这里只有一种任务类型和一个队列,比如nodejs里有好几种。
3 没有任务的时候怎么处理?进入睡眠,而不是真的是一个死循环。

其中第3点是事件循环系统中非常重要的逻辑。因为事件循环是属于生产者、消费者模式。任务队列中不可能一直都有任务需要处理,这就意味着生产任务可以是一个异步的过程。所以事件循环系统就需要有一种等待的机制。这就会带来两个问题,什么情况下需要等待,什么时候需要退出。这个和具体的业务场景有关,本文实现的事件循环中,没有任务的时候就会一直等待,而不是退出。除非用户手动执行setStop退出。而nodejs中,如果没有actived状态的handle和request并且close阶段没有任务时就会自动退出。另外一个问题就是如何实现等待。这里使用的是setTimeout来模拟睡眠,从而达到等待的效果。但是这时候进程是没有被挂起的,这意味着,我们还可以做其他事情。而在nodejs中,会在poll io阶段,进程会被挂起。我们看看nodejs事件循环的实现。

  while (r != 0 && loop->stop_flag == 0) {
    uv__update_time(loop);
    uv__run_timers(loop);
    ran_pending = uv__run_pending(loop);
    uv__run_idle(loop);
    uv__run_prepare(loop);
​
    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop);
    // 这里会导致事件循环系统所在进程挂起
    uv__io_poll(loop, timeout);
    uv__run_check(loop);
    uv__run_closing_handles(loop);
    r = uv__loop_alive(loop);
  }


nodejs的事件循环也是一个while循环,然后在里面执行各个阶段的任务,其中uv__io_poll对应的poll io阶段可能会导致进程挂起。我们看一下uv__io_poll关于等待的逻辑。

nfds = epoll_wait(loop->backend_fd,
                        events,
                        ARRAY_SIZE(events),
                        timeout);

epoll_wait会根据timeout的值决定如果没有就绪事件时,是否需要挂起进程。timeout大于0说明是定时器挂起,timeout等于-1说明是永远挂起。直到有就绪队列。这就是nodejs中关于等待的处理逻辑。这和我们自己实现的事件循环系统是类似的,只不过我们是自己唤醒自己,而nodejs中是被操作系统唤醒,因为我们在js层面无法调用操作系统的系统调用挂起进程。epoll是和文件描述符相关的,如果我们不涉及到文件、网络操作,那么我们又如何实现等待呢?我们从libuv的线程池实现中,找到了另一种实现。libuv的线程池中有多个线程,他们共享一个任务队列,每个子线程里不断从共享的任务队列中获取任务处理(需要加锁)。所以这也是一个事件循环的模型。那么当没有任务可处理的时候,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 (;;) {
    // 没有任务或者某类任务达到阈值
    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;
    }
  }
}

我们看到libuv使用线程库提供的api实现了线程的挂起。从而实现了等待的逻辑。接下来我们看一下唤醒的逻辑。

static void post(QUEUE* q, enum uv__work_kind kind) {
  uv_mutex_lock(&mutex);
  QUEUE_INSERT_TAIL(&wq, q);
  if (idle_threads > 0)
    uv_cond_signal(&cond);
  uv_mutex_unlock(&mutex);
}

libuv每次提交新的任务到共享队列时,都会判断是否有空闲线程,如果有则唤醒他。

本文介绍了事件循环的设计和实现中涉及到的一些知识,我们看到事件循环的整体架构是类似的,但是具体实现有很多种方式,这取决于你的业务场景。同时,任务的生产是异步的,所以没有任务的时候的等待机制的设计也就变得很重要,我们不能不断地浪费cpu进行轮询,而是要借助一直挂起,唤醒的机制来实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值