Node.js知識(shí) — 如何實(shí)現(xiàn)線程睡眠?
本文轉(zhuǎn)載自微信公眾號(hào)「Nodejs技術(shù)棧」,作者五月君。轉(zhuǎn)載本文請(qǐng)聯(lián)系Nodejs技術(shù)棧公眾號(hào)。
Node.js 小知識(shí) 記錄一些工作中或 “Nodejs技術(shù)棧” 交流群中大家遇到的一些問(wèn)題,有時(shí)一個(gè)小小的問(wèn)題背后也能延伸出很多新的知識(shí)點(diǎn),解決問(wèn)題和總結(jié)的過(guò)程本身也是一個(gè)成長(zhǎng)的過(guò)程,在這里與大家共同分享成長(zhǎng)。
使用 JavaScript/Node.js 的開(kāi)發(fā)者如果遇到需要實(shí)現(xiàn)延遲的任務(wù),可能會(huì)有疑問(wèn)🤔️ 為什么這里沒(méi)有類(lèi)似 Java 中 Thread.sleep() 這樣的方式來(lái)實(shí)現(xiàn)線程睡眠,本文講解如何在 Node.js 中實(shí)現(xiàn)一個(gè) sleep() 函數(shù)。
一:糟糕的 “循環(huán)空轉(zhuǎn)”
下面這段代碼是糟糕的,Node.js 是以單進(jìn)程、單線程的方式啟動(dòng),所有的業(yè)務(wù)代碼都工作在主線程,這樣會(huì)造成 CPU 持續(xù)占用,主線程阻塞對(duì) CPU 資源也是一種浪費(fèi),與真正的線程睡眠相差甚遠(yuǎn)。
- const start = new Date();
 - while (new Date() - start < 2000) {}
 
運(yùn)行之后如上圖所示,CPU 暴漲,同時(shí)也會(huì)破壞事件循環(huán)調(diào)度,導(dǎo)致其它任務(wù)無(wú)法執(zhí)行。
二:定時(shí)器 + Promise 實(shí)現(xiàn) sleep
通過(guò)定時(shí)器延遲執(zhí)行函數(shù) setTimeout + Promise 的鏈?zhǔn)揭蕾?lài)實(shí)現(xiàn),本質(zhì)是創(chuàng)建一個(gè)新的 Promise 對(duì)象,待定時(shí)器延遲時(shí)間到了執(zhí)行 resolve 函數(shù)這時(shí) then 才會(huì)執(zhí)行,這里 Node.js 執(zhí)行線程是沒(méi)有進(jìn)行睡眠的,事件循環(huán)和 V8 等都是正常運(yùn)行的。但這也是目前通用的一種解決方案,因?yàn)槟悴荒茏屩骶€程阻塞,否則程序就無(wú)法繼續(xù)工作了。
- const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
 
在 Node.js 中還可以利用 util 模塊提供的 promisify 方法實(shí)現(xiàn),一種快捷方式。
- const { promisify } = require('util');
 - const sleep = promisify(setTimeout);
 
因?yàn)槭腔诙〞r(shí)器與 Promise 所以也自然是異步的方式了,使用時(shí)也要注意,如下所示:
- // async await 的方式
 - async function test() {
 - console.log(1);
 - await sleep(3000);
 - console.log(2);
 - }
 - // Promise 的鏈?zhǔn)秸{(diào)用方式
 - async function test() {
 - console.log(1);
 - sleep(3000).then(() => {
 - console.log(2);
 - });
 - }
 
三:零 CPU 開(kāi)銷(xiāo)真正的事件循環(huán)阻止 sleep 實(shí)現(xiàn)
ECMA262 草案提供了 Atomics.wait API 來(lái)實(shí)現(xiàn)線程睡眠,它會(huì)真正的阻塞事件循環(huán),阻塞線程直到超時(shí)。
該方法 Atomics.wait(Int32Array, index, value[, timeout]) 會(huì)驗(yàn)證給定的 Int32Array 數(shù)組位置中是否仍包含其值,在休眠狀態(tài)下會(huì)等待喚醒或直到超時(shí),返回一個(gè)字符串表示超時(shí)還是被喚醒。
同樣的因?yàn)槲覀兊臉I(yè)務(wù)是工作在主線程,避免在主線程中使用,在 Node.js 的工作線程中可以根據(jù)實(shí)際需要使用。
- /**
 - * 真正的阻塞事件循環(huán),阻塞線程直到超時(shí),不要在主線程上使用
 - * @param {Number} ms delay
 - * @returns {String} ok|not-equal|timed-out
 - */
 - function sleep(ms) {
 - const valid = ms > 0 && ms < Infinity;
 - if (valid === false) {
 - if (typeof ms !== 'number' && typeof ms !== 'bigint') {
 - throw TypeError('ms must be a number');
 - }
 - throw RangeError('ms must be a number that is greater than 0 but less than Infinity');
 - }
 - return Atomics.wait(int32, 0, 0, Number(ms))
 - }
 - sleep(3000)
 
由于本節(jié)我們僅是在講解 sleep 的實(shí)現(xiàn),所以關(guān)于 Atomics.wait 方法睡眠之后如何被其它線程喚醒也不再此處講了,之后我會(huì)寫(xiě)一講 Node.js 中的工作線程相關(guān)文章,到時(shí)會(huì)再次介紹。
四:基于 N-API 擴(kuò)展使用 C 語(yǔ)言實(shí)現(xiàn) sleep
通過(guò) Addon 的方式使用 N-API 編寫(xiě) C/C++ 插件,借助其提供的系統(tǒng) sleep() 函數(shù)實(shí)現(xiàn)。
- // sleep.c
 - #include <assert.h>
 - #include <unistd.h>
 - #include <node_api.h>
 - napi_value sleepFn(napi_env env, napi_callback_info info) {
 - napi_status status;
 - size_t argc = 1;
 - napi_value argv[1];
 - status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
 - assert(status == napi_ok);
 - if (argc < 1) {
 - napi_throw_type_error(env, NULL, "ms is required");
 - return NULL;
 - }
 - napi_valuetype valueType;
 - napi_typeof(env, argv[0], &valueType);
 - if (valueType != napi_number) {
 - napi_throw_type_error(env, NULL, "ms must be a number");
 - return NULL;
 - }
 - int64_t s;
 - napi_get_value_int64(env, argv[0], &s);
 - sleep(s);
 - return NULL;
 - }
 - napi_value init(napi_env env, napi_value exports) {
 - napi_status status;
 - napi_property_descriptor descriptor = {
 - "sleep",
 - 0,
 - sleepFn,
 - 0,
 - 0,
 - 0,
 - napi_default,
 - 0
 - };
 - status = napi_define_properties(env, exports, 1, &descriptor);
 - assert(status == napi_ok);
 - return exports;
 - }
 - NAPI_MODULE(sleep, init);
 
經(jīng)過(guò)一系列編譯之后,引入 .node 文件直接使用。
- // app.js
 - const { sleep } = require('./build/Release/sleep.node');
 - sleep(3);
 
五:easy-sleep 模塊
這是筆者寫(xiě)的一個(gè)小模塊 https://github.com/qufei1993/easy-sleep,其實(shí)也是對(duì)以上幾種方法的整合,包含了 C 插件的編寫(xiě),使用如下:
- // Install
 - npm install easy-sleep -S
 - // Async sleep
 - const { sleep } = require('easy-sleep');
 - await sleep(3000);
 - // Thread sleep
 - const { Thread } = require('easy-sleep');
 - Thread.sleep();
 
總結(jié)
由于 JavaScript 是單線程的語(yǔ)言,通常我們都是工作在主線程,如果真的讓線程睡眠了,事件循環(huán)也會(huì)被阻塞,后續(xù)的程序就無(wú)法正常工作了,大多數(shù)情況,我們也是簡(jiǎn)單的對(duì) setTimeout 函數(shù)做一些封裝實(shí)現(xiàn)延遲功能。在瀏覽器/Node.js 的工作線程下可以根據(jù)實(shí)際需要決定是否需要工作線程睡眠。
















 
 
 





 
 
 
 