JavaScript为什么是单线程的?

59

我有一个关于JavaScript单线程特性的问题。

console.log("1");
setTimeout(function(){console.log("2");},3000);
console.log("3");
setTimeout(function(){console.log("4");},1000);
这段代码的结果是 1 3 4 2。可以看到,4 出现在 2 后面,这让我想知道在单线程环境下,2 不应该出现在 4 后面吗?如果不是这样,那么 JavaScript 是如何知道第二个 setTimeout 应该在第一个之前完成呢?难道不应该有两个线程并发工作以按顺序完成这两个 setTimeout 并通知 EventLoop 吗?

setTimeout 只是安排一个任务。将 "2" 安排在未来的 3000ms,不会阻塞其他代码的执行。 - cookie monster
11
只有在同时执行多个任务时,它才会是多线程的,但你的代码并没有同时执行多个任务。 - user253751
如果代码被安排在同一时间运行,或者意外地,在超时时刻有一些代码正在执行,会发生什么?在这种情况下,它如何决定执行顺序? - tudor -Reinstate Monica-
如果我在这个例子的结尾(愚蠢地)添加pausecomp(999)会怎么样?(请参见http://www.sean.co.uk/a/webdesign/javascriptdelay.shtm) - tudor -Reinstate Monica-
多线程和异步是两个不同的概念。 - WiSeeker
这个视频以非常好的方式进行了解释。https://youtu.be/cCOL7MC4Pl0 - theshubhagrwl
6个回答

39

JavaScript (在浏览器中) 不会并发运行2

最多只有setTimeout回调之一可以在同一时间执行 - 因为只有一个JavaScript执行上下文或“线程”。

但是,“下一个计划的超时”始终会被先执行。 "4" 在 "2" 回调之前运行,因为它被安排在更早运行。这些超时实际上是从相同的时间开始安排的(没有任何操作阻塞),但 "2" 的间隔要长得多。

底层的实现可能使用线程1 - 但在同一全局上下文中的JavaScript 不会并发运行,并保证所有回调之间的一致性原子性行为。


1 或者也可以不用;这可以在使用select/poll实现时处理而不需要任何线程。

2 在同一上下文中:即标签/窗口、WebWorker、宿主浏览器控制。例如,虽然WebWorkers是并发运行的,但它们是在不同的上下文中运行,并遵循相同的异步模型(例如计时器使用的模型)。


14
我们都依赖于这种行为并予以观察,但是它的记录在哪里呢? - Dr. Jan-Philip Gehrcke

26

Javascript使用称为Eventloop的东西进行异步调用。 由于setTimeout是回调函数,因此将其推送到EventLoop中。主线程继续执行。一旦主线程完成,EventLoop就会将数据推送到主堆栈中。 例如:

console.log("1");
setTimeout(function(){console.log("2");},0);
console.log("3");
setTimeout(function(){console.log("4");},1000);

当超时时间为0时,代码的输出将是:

1 3 2 4

因为它首先执行Main调用,然后从Eventloop返回数据。 并发模型和事件循环


13

Javascript按顺序执行每一行代码。

因此,你让js做了以下事情:

  • 写1:js写入1
  • 等待3秒,然后写2:好的,我会等3秒...现在怎么办?
  • 写3:好的,我会写3,顺便说一下,3秒还没到。
  • 等待1秒,然后写4:好的,我会等1秒...

然后js等待0.99999秒...并写入了4

然后再等一段时间,写入2


4
顺便提一下,“顶多3秒钟”这句话是误导性的。即使定时器的时间到了,JavaScript也不可能在那里执行任何操作,因为JS是单线程的。JS线程只能处于代码内或代码外;当它处于代码内部时,它不能执行其他任何操作。只有当您的代码退出最外层函数时,任何超时才会发生(因为此时您的代码已经释放了对JS引擎的控制)。 - Amadan
这是一个非常不令人满意的解释。setTimeout 允许多个重叠的超时同时运行,这怎么办?还有那些依赖于外部源(例如 XMLHttpRequest 结果)的事件回调,必须能够随时启动,但仍在同一线程上运行的事实呢?无意冒犯,但这个答案感觉太笼统了。 - Shien
@Shien setTimeout() 仍然是单线程运行的,因此它们不能保证在确切的毫秒时刻触发。为了更好地理解,请参考:http://ejohn.org/blog/how-javascript-timers-work/ - Brian McGinity

7
setTimeout的第二个参数是最小时间,之后回调函数(第一个参数)将被推送到事件循环中,这是回调函数队列。该队列被消耗以实际开始执行。一旦遇到第一个setTimeout,该函数会被推送到某个地方,并告诉它在重新进入单线程世界之前等待3秒钟。第二个超时函数也发生了同样的事情,但只需等待1秒。这个单线程世界的入口点是回调队列。JS引擎继续正常执行,就像settimeout执行已经完成一样。现在,一旦1秒过去,第二个timeout的函数被推送到队列中等待执行。如果此时调用堆栈清除,则该函数会进行处理(假设它是队列的第一个成员),并打印“4”。如果在这段时间内没有过了3秒钟,第一个timeout的函数仍然在外面等待。一旦3秒过去,回调函数进入队列,由于调用堆栈为空,它执行并打印“2”。现在浏览器可以从OS访问多个线程(尽管为JS执行提供了单线程环境)。这些setTimeout由幕后的另一个线程处理。

Philips Robert制作了一段精美的视频,清晰解释了队列和事件循环的概念,这些概念导致了单线程JavaScript的“异步性”。

https://www.youtube.com/watch?v=8aGhZQkoFbQ


0

定义 - 多线程: 多线程是一种执行模型,允许多个线程存在于进程的上下文中,以便它们独立执行但共享其进程资源。 从这个意义上说,JavaScript 是完全多线程的东西:

   let Ar;
    Ar = []

    function multiapp(ar,w,n) {
        ar.clear;
        for (let i = 0; i < n; i++)
            setTimeout(function(){ar.push(w)},i);
    }

    +function fn() {
        //Thread 1
        setTimeout(() => multiapp(Ar,1,10),100)
        //Thread 2
        setTimeout(() => multiapp(Ar,2,10),100)
    }()

    setTimeout(function(){console.log('Ar(1) = ', Ar.filter((i) => i == 1).length,'Ar(2) = ', Ar.filter((i) => i == 2).length,' Ar = ',Ar)},2000);
//Ar(1) =  10 Ar(2) =  10  Ar =  [ 1, 1, 2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2 ]`

显然,线程1启动并执行自己的堆栈,与线程2独立。两个线程共享对同一上下文对象实例Ar的访问,并以未知的方式修改它。行为与具有原子基元的Java完全相同。


0

以下是步骤:

  1. 将console.log(1)添加到JS调用堆栈中。时间(~0)

  2. 执行它。(在控制台中打印1)- 时间(~0)

  3. 将setTimeout(function(){console.log("2");},3000);添加到调用堆栈中。- 时间(~0)

  4. 将其移动到事件循环并启动计时器。- 时间(3秒)

由于setTimeout是异步函数,因此它将移动到事件循环中。

  1. 将console.log(3)添加到JS调用堆栈中。时间(~0)

  2. 执行它。(在控制台中打印3) 时间(~0)

  3. 将setTimeout(function(){console.log("4");},1000);添加到调用堆栈中。时间(~0)

  4. 将其移动到事件循环并启动计时器。- 时间(1秒)

  5. 1秒计时器完成后,它将返回到调用堆栈并被执行。

  6. 调用堆栈执行它。(在控制台中打印4) - 时间(~0)

  7. 3秒计时器完成后,它将返回到调用堆栈并被执行。

  8. 调用堆栈执行它。(在控制台中打印2) - 时间(~0)

现在我们所说的同步是JS调用堆栈,它一次只能执行一件事。

我本可以将其变成一个20步骤的过程,但为了易于理解,12步就足够了。


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