如何在node.js中让线程休眠而不影响其他线程?

83
根据《理解node.js事件循环》,node.js支持单线程模型。这意味着如果我向node.js服务器发出多个请求,它不会为每个请求生成一个新线程,而是逐个执行每个请求。也就是说,如果我在我的node.js代码中对第一个请求执行以下操作,同时在node上出现了一个新的请求,那么第二个请求必须等待直到第一个请求完成,包括5秒的睡眠时间。 对吗?
var sleep = require('sleep');
    sleep.sleep(5)//sleep for 5 seconds

是否有一种方法可以让node.js为每个请求生成一个新的线程,从而使第二个请求不必等待第一个请求完成,或者我只能在特定的线程上调用sleep吗?


4
你不需要像那样睡觉。你应该看看setTimeout -> http://nodejs.org/api/globals.html#globals_settimeout_cb_ms - Alfred
5个回答

176

如果您指的是npm模块sleep,则在自述文件中注意到sleep将会阻止执行。所以您是正确的 - 它不是您想要的。相反,您应该使用非阻塞的setTimeout。以下是一个示例:

setTimeout(function() {
  console.log('hello world!');
}, 5000);

如果有人想使用 es7 的 async/await 实现此操作,可以参考以下示例:

const snooze = ms => new Promise(resolve => setTimeout(resolve, ms));

const example = async () => {
  console.log('About to snooze without halting the event loop...');
  await snooze(1000);
  console.log('done!');
};

example();

那么Node.js会一次执行一个请求,这意味着它遵循单线程模型。对吗? - emilly
7
是的,它使用单个执行线程,因此一次只能计算一个请求,但可能有多个请求上挂起事件。当您调用setTimeout时,它会将事件存储在队列中,然后可以开始执行下一个请求。五秒钟后,事件将从队列中删除,并将执行回调(在这种情况下包含console.log的函数)。 - David Weldon
7
它不会阻塞线程。在这个 setTimeout 后面的代码会继续执行,然后在 5 秒后控制台打印出 "hello world"。 - CodeFarmer
1
@DavidWeldon,Node是如何决定在将setTimeout从队列中移除后,在某个时间点触发它的执行的呢?换句话说,如果它不处于执行模式/上下文中,是什么触发了函数在五秒后运行? - securecurve
@Pacerier 那个包似乎有一个低级别的v8绑定来暂停执行而不需要繁忙循环,但从概念上讲,你可以这样考虑。无论哪种方式,sleep都不应该出现在生产代码中,因为它在运行时会阻塞并发执行。 - David Weldon
显示剩余3条评论

3

这是一个相当老的问题,尽管被接受的答案仍然完全正确,但在v15中添加的timers/promises API提供了一种更简单的方法。

import { setTimeout } from 'timers/promises';

// non blocking wait for 5 secs
await setTimeout(5 * 1000);

1
请考虑使用deasync模块,个人不喜欢Promise方式使所有函数异步化,也不喜欢随处使用关键字async/await。我认为官方的node.js应该考虑公开事件循环API,这将简单地解决回调地狱问题。Node.js是一个框架而不是一种语言。
var node = require("deasync");
node.loop = node.runLoopOnce;

var done = 0;
// async call here
db.query("select * from ticket", (error, results, fields)=>{
    done = 1;
});

while (!done)
    node.loop();

// Now, here you go

1
如果您有一个循环,每个循环中都有一个异步请求,并且您想要在每个请求之间设置一定的时间间隔,您可以使用以下代码:
   var startTimeout = function(timeout, i){
        setTimeout(function() {
            myAsyncFunc(i).then(function(data){
                console.log(data);
            })
        }, timeout);
   }

   var myFunc = function(){
        timeout = 0;
        i = 0;
        while(i < 10){
            // By calling a function, the i-value is going to be 1.. 10 and not always 10
            startTimeout(timeout, i);
            // Increase timeout by 1 sec after each call
            timeout += 1000;
            i++;
        }
    }

这个例子在每次请求后等待1秒钟再发送下一个请求。

这个代码 不会 像你期望的那样运行。循环并没有异步执行,它会立即触发所有请求。当考虑使用异步循环时一定要非常小心 -- 这种组合很棘手。 - treecoder
当然,循环会一次性调用。但是超时时间每次循环都会增加。所以1秒...2秒...3秒。这就是为什么“myAsyncFunc”每秒都会被调用,而不是一次性全部调用。当我测试它时,它的工作效果符合预期。你能告诉我你认为哪里有问题吗? - Jodo
1
顺便说一下,代码确实可以工作,但有点棘手。_startTimeout_函数会立即被调用10次,但每次调用的超时时间都比上一次多1秒,因此对_myAsyncFunc_的每次调用都会延迟1秒。 - greuze
1
但这违背了循环的整个目的。你应该能够决定循环运行的次数。你上面所做的将精确地执行循环10次--无论你是否需要10次迭代。你不能在前一个迭代结束时决定是否要进行另一次迭代--因为你不知道前一个迭代何时结束。因此,你必须提前决定有多少次迭代会发生。迭代中断逻辑将泄漏到主题回调中--在你的情况下是myAsyncFunc - treecoder
1
第二个缺陷是你假设你的回调函数不会超过1秒钟执行时间。如果你的下一次迭代依赖于上一次迭代的结果,而且这个结果不能保证在1秒钟内可用,那么你的下一次迭代将在没有必要的结果的情况下开始——因为它将在前一次迭代后恰好1秒钟开始,而不管从前一次迭代调用的回调是否已经结束。 - treecoder

0
当使用第三方库提供的异步函数或可观察对象时,例如Cloud Firestore,在需要等待某个过程完成但又不想在回调中嵌套回调或者冒着陷入无限循环的风险时,我发现下面所示的waitFor方法(TypeScript编写,但你明白我的意思……)非常有用。
该方法有点类似于while (!condition)睡眠循环,但它是异步执行的,并且会在一定时间间隔内反复进行完成条件测试,直到为真或超时。
export const sleep = (ms: number) => {
    return new Promise(resolve => setTimeout(resolve, ms))
}
/**
 * Wait until the condition tested in a function returns true, or until 
 * a timeout is exceeded.
 * @param interval The frenequency with which the boolean function contained in condition is called.
 * @param timeout  The maximum time to allow for booleanFunction to return true
 * @param booleanFunction:  A completion function to evaluate after each interval. waitFor will return true as soon as the completion function returns true.   
 */
export const waitFor = async function (interval: number, timeout: number,
    booleanFunction: Function): Promise<boolean> {
    let elapsed = 1;
    if (booleanFunction()) return true;
    while (elapsed < timeout) {
        elapsed += interval;
        await sleep(interval);
        if (booleanFunction()) {
            return true;
        }
    }
    return false;
}

假设您的后端有一个长时间运行的进程,您希望在执行其他任务之前完成它。例如,如果您有一个函数来计算账户列表的总数,但是您想在计算之前从后端刷新账户,您可以像这样操作:
async recalcAccountTotals() : number {
     this.accountService.refresh();   //start the async process.
     if (this.accounts.dirty) {
           let updateResult = await waitFor(100,2000,()=> {return !(this.accounts.dirty)})
     }
 if(!updateResult) { 
      console.error("Account refresh timed out, recalc aborted");
      return NaN;
    }
 return ... //calculate the account total. 
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接