Node.js知識 — 如何實現線程睡眠?
本文轉載自微信公眾號「Nodejs技術棧」,作者五月君。轉載本文請聯系Nodejs技術棧公眾號。
Node.js 小知識 記錄一些工作中或 “Nodejs技術棧” 交流群中大家遇到的一些問題,有時一個小小的問題背后也能延伸出很多新的知識點,解決問題和總結的過程本身也是一個成長的過程,在這里與大家共同分享成長。
使用 JavaScript/Node.js 的開發者如果遇到需要實現延遲的任務,可能會有疑問🤔️ 為什么這里沒有類似 Java 中 Thread.sleep() 這樣的方式來實現線程睡眠,本文講解如何在 Node.js 中實現一個 sleep() 函數。
一:糟糕的 “循環空轉”
下面這段代碼是糟糕的,Node.js 是以單進程、單線程的方式啟動,所有的業務代碼都工作在主線程,這樣會造成 CPU 持續占用,主線程阻塞對 CPU 資源也是一種浪費,與真正的線程睡眠相差甚遠。
- const start = new Date();
- while (new Date() - start < 2000) {}
運行之后如上圖所示,CPU 暴漲,同時也會破壞事件循環調度,導致其它任務無法執行。
二:定時器 + Promise 實現 sleep
通過定時器延遲執行函數 setTimeout + Promise 的鏈式依賴實現,本質是創建一個新的 Promise 對象,待定時器延遲時間到了執行 resolve 函數這時 then 才會執行,這里 Node.js 執行線程是沒有進行睡眠的,事件循環和 V8 等都是正常運行的。但這也是目前通用的一種解決方案,因為你不能讓主線程阻塞,否則程序就無法繼續工作了。
- const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
在 Node.js 中還可以利用 util 模塊提供的 promisify 方法實現,一種快捷方式。
- const { promisify } = require('util');
- const sleep = promisify(setTimeout);
因為是基于定時器與 Promise 所以也自然是異步的方式了,使用時也要注意,如下所示:
- // async await 的方式
- async function test() {
- console.log(1);
- await sleep(3000);
- console.log(2);
- }
- // Promise 的鏈式調用方式
- async function test() {
- console.log(1);
- sleep(3000).then(() => {
- console.log(2);
- });
- }
三:零 CPU 開銷真正的事件循環阻止 sleep 實現
ECMA262 草案提供了 Atomics.wait API 來實現線程睡眠,它會真正的阻塞事件循環,阻塞線程直到超時。
該方法 Atomics.wait(Int32Array, index, value[, timeout]) 會驗證給定的 Int32Array 數組位置中是否仍包含其值,在休眠狀態下會等待喚醒或直到超時,返回一個字符串表示超時還是被喚醒。
同樣的因為我們的業務是工作在主線程,避免在主線程中使用,在 Node.js 的工作線程中可以根據實際需要使用。
- /**
- * 真正的阻塞事件循環,阻塞線程直到超時,不要在主線程上使用
- * @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)
由于本節我們僅是在講解 sleep 的實現,所以關于 Atomics.wait 方法睡眠之后如何被其它線程喚醒也不再此處講了,之后我會寫一講 Node.js 中的工作線程相關文章,到時會再次介紹。
四:基于 N-API 擴展使用 C 語言實現 sleep
通過 Addon 的方式使用 N-API 編寫 C/C++ 插件,借助其提供的系統 sleep() 函數實現。
- // 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);
經過一系列編譯之后,引入 .node 文件直接使用。
- // app.js
- const { sleep } = require('./build/Release/sleep.node');
- sleep(3);
五:easy-sleep 模塊
這是筆者寫的一個小模塊 https://github.com/qufei1993/easy-sleep,其實也是對以上幾種方法的整合,包含了 C 插件的編寫,使用如下:
- // 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();
總結
由于 JavaScript 是單線程的語言,通常我們都是工作在主線程,如果真的讓線程睡眠了,事件循環也會被阻塞,后續的程序就無法正常工作了,大多數情況,我們也是簡單的對 setTimeout 函數做一些封裝實現延遲功能。在瀏覽器/Node.js 的工作線程下可以根據實際需要決定是否需要工作線程睡眠。