ecmascript多线程-为什么javascript是单线程的?

2023-08-22 0 1,019 百度已收录

明天笔试的时候,笔试官问了我这个问题,为什么javascript是单线程的? 我的耳朵一下子蒙了,我从单线程和多线程的区别来回答:比如多线程要考虑线程之间的资源占用,死锁,冲突之类的。 我回到中学寻找这个问题的答案。 分享一下阮一峰老师的博客,原文地址:

1. 为什么JavaScript是单线程的?

JavaScript语言的一大特点是单线程,即同一时间只能做一件事。 那么,为什么 JavaScript 不能有多个线程呢? 这样可以提高效率。

JavaScript的单线程与其使用有关。 作为一种浏览器脚本语言,JavaScript 的主要目的是与用户交互并操作 DOM。 这就决定了它只能是单线程的,否则会带来非常复杂的同步问题。 例如,假设JavaScript同时有两个线程,一个线程向某个DOM节点添加内容,另一个线程删除这个节点,那么此时浏览器应该以哪个线程为准呢?

为了利用多核CPU的计算能力,HTML5提出了WebWorker标准,该标准允许JavaScript脚本创建多个线程,子线程完成

全部由主线程控制,并且一定不能操作DOM。 因此,这个新标准并没有改变 JavaScript 的单线程本质。

2. 任务队列

单线程意味着所有任务都需要排队,只有前一个任务完成后才能执行前一个任务。 如果前一个任务耗时较长,后一个任务还得等待。

如果排队是因为计算量大,CPU太忙,那就算了,CPU大部分时间都是空闲的,因为IO设备(输入输出设备)速度很慢(例如Ajax操作)从网络读取数据),必须等待结果下来才能继续。

JavaScript语言的设计者意识到,此时主线程完全可以忽略IO设备,挂起等待的任务,先运行前面的任务。 等待IO设备返回结果,然后返回继续执行被挂起的任务。

因此,所有任务可以分为两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。 同步任务是指在主线程上排队等待执行的任务。 只有执行完上一个任务后,才能执行下一个任务; 异步任务是指不进入主线程的“任务队列”(taskqueue)。 对于任务来说,只有“任务队列”通知主线程可以执行异步任务,该任务才会单步进入主线程执行。

具体来说,异步执行的运行机制如下。 (同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

ecmascript多线程-为什么javascript是单线程的?

1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

右图是主线程和任务队列的示意图。

只要主线程为空,它也会读取“任务队列”,这就是JavaScript的运行机制。 这个过程一遍又一遍地重复。

3.事件和弹跳功能

“任务队列”就是一个风暴队列(也可以理解为消息队列)。 当IO设备完成一个任务时,会在“任务队列”中添加一个storm,表示相关的异步任务可以进入“执行栈”。 。 主线程读取“任务队列”,就是读取正在发生的事情。

“任务队列”的混乱不仅包括IO设备的混乱,还包括某些用户引起的混乱(例如键盘点击、页面滚动等)。 只要指定了反弹函数,当出现这样的风暴时,就会进入“任务队列”等待主线程读取。

所谓“回调函数”(callback)就是会被主线程挂起的代码。 异步任务必须指定反弹函数。 当主线程开始执行异步任务时,就会执行相应的反弹函数。

“任务队列”是先进先出的数据结构,排在后面的storm首先被主线程读取。 主线程的读取过程基本上都是手动的。 一旦执行堆栈被清除,“任务队列”上的第一个风暴就会手动进入主线程。 而且,因为后面提到的“定时器”功能,主线程首先要检查执行时间,个别风暴只有到了指定的时间才会返回主线程。

4. 事件循环

主线程从“任务队列”中读取storm。 这个过程是循环的,所以整个运行机制也被称为EventLoop(风暴循环)。

为了更好地理解EventLoop,请查看右图。 (引自 Philip Roberts 的演讲“救命,我陷入了事件循环”)

上图中,主线程运行时,形成了堆和栈。 堆栈中的代码调用各种外部API,它们向“任务队列”添加各种干扰(点击、加载、完成)。 。 只要执行完栈中的代码,主线程就会读取“任务队列”,依次执行这些扰动对应的反弹函数。

执行栈(同步任务)中的代码总是在读取“任务队列”(异步任务)之前执行。 请看下面的例子。

    var req = new XMLHttpRequest();
    req.open('GET', url);    
    req.onload = function (){};    
    req.onerror = function (){};    
    req.send();

代码中的req.send方法是一个Ajax操作,用于向服务器发送数据。 它是一个异步任务,这意味着系统只有在当前脚本的所有代码执行完之后才会读取“任务队列”。 因此,相当于下面的写法。

 var req = new XMLHttpRequest();
    req.open('GET', url);
    req.send();
    req.onload = function (){};    
    req.onerror = function (){};   

也就是说,指定反弹函数的部分(onload和onerror)是在send()方法的上面还是旁边并不重要ecmascript多线程,因为它们都属于执行栈的一部分,系统总是会执行后阅读它们。 “任务队列”。

5. 定时器

ecmascript多线程-为什么javascript是单线程的?

“任务队列”不仅可以放置异步任务风暴,还可以放置定时风暴,即指定单个代码执行多长时间。 这称为“定时器”(timer)函数,即定期执行的代码。

定时器功能主要由setTimeout()和setInterval()这两个函数完成,它们的内部运行机制是完全一样的,不同的是后者指的是

指定的代码执行一次,而前者则重复执行。 下面主要讨论setTimeout()。

setTimeout() 接受两个参数,第一个是反弹函数,第二个是延迟执行的微秒数。

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

里面代码的执行结果是1,3,2,因为setTimeout()将第二行的执行延迟到了1000微秒之后。

如果setTimeout()的第二个参数设置为0,则表示当前代码执行完后(清空执行堆栈),立即执行指定的反弹函数(0微秒间隔)。


setTimeout(function(){console.log(1);}, 0);
console.log(2);

里面代码的执行结果始终是2, 1,因为只有执行到第二行之后,系统才会执行“任务队列”中的反弹函数。

其实setTimeout(fn, 0)的意义就是指定一个任务在主线程最早可用空闲时间执行,即尽早执行。 它将一个wave添加到“任务队列”的末尾,因此只有在同步任务和“任务队列”的现有wave都处理完毕后才能执行。

ecmascript多线程-为什么javascript是单线程的?

HTML5标准规定setTimeout()第二个参数的最小值(最短间隔)不得高于4微秒,高于此值则手动减小。 在此之前,较旧的浏览器将最小间隔设置为 10 微秒。 另外,这些 DOM 更改(尤其是涉及页面重新渲染的更改)一般不会立即执行,而是每 16 纳秒执行一次。 这时使用requestAnimationFrame()的效果比setTimeout()要好。

需要注意的是,setTimeout()只是将storm插入到“任务队列”中,主线程必须等到当前代码(执行栈)执行完毕后,主线程才能执行其指定的反弹函数。 如果当前代码耗时较长,则可能会耗时较长,因此无法保证反弹函数一定会在setTimeout()指定的时间执行。

6.Node.js的EventLoop

Node.js也是单线程的EventLoop,其运行机制与浏览器环境不同。

请参见下图(作者:@BusyRich)。

根据上图,Node.js的运行机制如下。

1)V8引擎解析JavaScript脚本。
(2)解析后的代码,调用Node API。
(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
(4)V8引擎再将结果返回给用户。

不仅仅是setTimeout和setInterval这两个方法,Node.js还提供了另外两个与“任务队列”相关的方法:process.nextTick和setImmediate。 它们可以帮助我们加深对“任务队列”的理解。

process.nextTick方法可以在下一个EventLoop(主线程读取“任务队列”)之前在当前“执行栈”末尾触发反弹函数。 也就是说,它指定的任务总是发生在所有异步任务之前。 setImmediate方法是在当前“任务队列”的末尾添加一个storm,即它指定的任务总是在下一个EventLoop中执行,这与setTimeout(fn,0)非常相似。 请参阅下面的示例(viaStackOverflow)。

ecmascript多线程-为什么javascript是单线程的?


process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});
setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED

里面的代码中,由于process.nextTick指定的弹跳函数总是在当前“执行栈”末尾触发,所以除了函数A在setTimeout指定的弹跳函数超时之前执行之外,函数B也被执行超时之前。 这意味着如果有多个process.nextTick语句(无论是否嵌套),它们都会在当前“执行栈”上执行。

现在,再次查看 setImmediate。


setImmediate(function A() {
  console.log(1);
  setImmediate(function B(){console.log(2);});
});
setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0);

里面的代码中,setImmediate和setTimeout(fn, 0)分别添加了反弹函数A和超时,这两个函数都是在下一个EventLoop中触发的。 那么,哪个反弹函数首先执行呢? 答案并不确定。 操作的结果可能是 1–TIMEOUTFIRED–2 或 TIMEOUTFIRED–1–2。

麻烦的是 Node.js 文档说 setImmediate 指定的反弹函数总是在 setTimeout 之后。 事实上,这些情况只发生在递归调用时。


setImmediate(function (){
  setImmediate(function A() {
    console.log(1);
    setImmediate(function B(){console.log(2);});
  });
  setTimeout(function timeout() {
    console.log('TIMEOUT FIRED');
  }, 0);
});
// 1
// TIMEOUT FIRED
// 2

里面的代码中,setImmediate和setTimeout封装在一个setImmediate上,它的运行结果始终是1-TIMEOUTFIRED-2,此时函数A必须超时触发。 至于TIMEOUTFIRED旁边的2(即函数B在超时之前触发)ecmascript多线程,是因为setImmediate总是将storm注册到下一轮EventLoop中,所以函数A和超时在同一轮循环中执行,而函数B正在下一轮循环中执行。

由此我们得到process.nextTick和setImmediate之间的一个重要区别:多个process.nextTick语句总是在当前“执行栈”中执行一次,而多个setImmediate可能需要多次循环才能执行。 其实这就是Node.js10.0添加了setImmediate方法的原因,否则像下面这种对process.nextTick的递归调用将会无穷无尽,主线程永远不会读取“事件队列”!


process.nextTick(function foo() {
  process.nextTick(foo);
});

事实上,今天如果你递归地编写 process.nextTick,Node.js 会抛出一个警告,要求你更改为 setImmediate。

另外,由于process.nextTick指定的bounce函数是在本次“事件循环”中触发的,而setImmediate指定是在最后一个“事件循环”中触发的,所以实际上后者总是早于前者发生,但是执行效率也很高(因为不需要检测“任务队列”)。

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 ecmascript ecmascript多线程-为什么javascript是单线程的? https://www.wkzy.net/game/140217.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务