首发于What Matters
atomic-sleep: 一个名副其实的JavaScript sleep函数

atomic-sleep: 一个名副其实的JavaScript sleep函数

某些情况下,我们需要让程序休眠一段时间再继续运行。

Node.js中通常的实现是这样的:

function sleep(ms) {
  return new Promise(res => setTimeout(res, ms));
}

如果你需要休息2秒,则这样使用:

// do something

await sleep(2000);

// do something else

但实际上该 `sleep` 函数只对当前调用有效,无法阻止其它异步函数的继续执行。一个简单的证明:

[1, 2, 3].map(async v => {
  console.log(new Date(), "sleep", v, "start");
  await sleep(2000);
  console.log(new Date(), "sleep", v, "over");
});

运行上述代码,终端上出现如下信息:

我们看到,尽管 1 中调用了 sleep23 几乎也在同一时间在运行着。尽管我们一共调用了3次 sleep,但整个程序基本上在2秒内就运行结束了。简单的解释,就这是这个版本的 sleep 并不能阻止 event loop 继续运行。

那么问题是,有没有好的办法阻塞 event loop 呢?

Yes,有人写了 atomic-sleep 模块:

davidmarkclements/atomic-sleepgithub.com图标

作者对它的介绍是:

⏱️Zero CPU overhead, zero dependency, true event-loop blocking sleep ⏱️

即,零 CPU 开销、零依赖、真正阻塞event loop的sleep


将上面代码中的 sleep 替换成 atomic-sleep 中的实现。

const sleep = require("atomic-sleep");

[1, 2, 3].map(async v => {
  console.log(new Date(), "sleep", v, "start");
  // 不需要加 await 关键字
  sleep(2000);
  console.log(new Date(), "sleep", v, "over");
});

我们看看打印了什么:

可以看到,整个过程用了6秒,1sleep 的过程中,23 被阻塞了。


源码解释

打开 atomic-sleep 模块的源码,在 index.js 文件中,我们看到其主要逻辑是依赖了 Atomics.wait 方法,该方法会监听一个Int32Array 对象的给定下标下的值,若值未发生改变,则一直等待(阻塞event loop),直到发生超时(由ms参数决定定):

const nil = new Int32Array(new SharedArrayBuffer(4))

function sleep (ms) {

  // 参数校验相关代码略去

  Atomics.wait(nil, 0, 0, Number(ms))
}

另外,模块还对低版本的 js 运行时做了兼容,如果不支持 Atomics,则改用一个占用CPU运行的方式:

function sleep(ms) {

  // 参数校验相关代码略去

  const target = Date.now() + Number(ms);
  while (target > Date.now()) {}
}

文章开头有说到,某些情况下,我们需要让程序休眠一段时间再继续运行。我提供一个具体的例子。

在写爬虫时,为了提升效率,我们通常会并发爬取一组网页。如果某个网页的请求返回了 429 异常码(Too Many Requests)。则休眠X秒再重试。我们已经知道基于 setTimeout 的休眠并不能阻止其它异步函数的执行(重试),这时 atomic-sleep 就派上用场了。

大致的爬取代码类似下面这样:

// 一组url
const urls = ["x", "y", "z", "..."];

// 并发爬取
const all_promised = urls.map(crawl);

// 获取爬取结果
const results = await Promise.all(all_promised);

// 爬取给定url
async function crawl(url) {
  const res = await fetch(url);
  if (res.status === 429) {
    // 发现异常,休眠重试
    await sleep(2000);
    // 先不考虑无限重试
    return crawl(url);
  }
  // 返回正常的结果
  return "bla bla bla";
}

觉得有用?欢迎关注我的公众号,上面有更多技巧性文章!

发布于 03-10

文章被以下专栏收录