JavaScript单线程和Event loop

单线程你联想到什么?

哦!操作系统课上讲过?对

  1. 线程和进程:

    进程和线程都是操作系统的概念。进程是应用程序的执行实例,每一个进程都是由私有的虚拟地址空间、代码、数据和其它系统资源所组成;进程在运行过程中能够申请创建和使用系统资源(如独立的内存区域等),这些资源也会随着进程的终止而被销毁。而线程则是进程内的一个独立执行单元,在不同的线程之间是可以共享进程资源的,所以在多线程的情况下,需要特别注意对临界资源的访问控制。在系统创建进程之后就开始启动执行进程的主线程,而进程的生命周期和这个主线程的生命周期一致,主线程的退出也就意味着进程的终止和销毁。主线程是由系统进程所创建的,同时用户也可以自主创建其它线程,这一系列的线程都会并发地运行于同一个进程中。

    显然,在多线程操作下可以实现应用的并行处理,从而以更高的CPU利用率提高整个应用程序的性能和吞吐量。特别是现在很多语言都支持多核并行处理技术,然而JavaScript却以单线程执行,为什么呢?

  2. Why?JavaScript单线程
    用途决定:
    作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
    所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
    为了利用多核CPU的计算能力,HTML5的Web
    Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

    并发模式与Event Loop

    javaScript 有个基于“Event Loop”并发的模型。
    啊,并发?不是说 JavaScript是单线程吗? 没错,的确是单线程,但是并发与并行是有区别的。
    前者是逻辑上的同时发生,而后者是物理上的同时发生。所以,单核处理器也能实现并发。
    mark
    并行大家都好理解,而所谓“并发”是指两个或两个以上的事件在同一时间间隔中发生。如上图的第一个表,由于计算机系统只有一个CPU,故ABC三个程序从“微观”上是交替使用CPU,但交替时间很短,用户察觉不到,形成了“宏观”意义上的并发操作。

    javaScipt怎样实现并发模式呢?

    答案:JavaScript Runtime
    mark
  3. Stack(栈)
    这里放着JavaScript正在执行的任务。每个任务被称为帧(stack of frames)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function f(b){
    var a = 12;
    return a+b+35;
    }
    function g(x){
    var m = 4;
    return f(m*x);
    }
    g(21);

上述代码调用 g 时,创建栈的第一帧,该帧包含了 g 的参数和局部变量。当 g 调用 f 时,第二帧就会被创建,并且置于第一帧之上,当然,该帧也包含了 f 的参数和局部变量。当 f 返回时,其对应的帧就会出栈。同理,当 g 返回时,栈就为空了(栈的特定就是后进先出 Last-in first-out (LIFO))。

  1. Heap(堆)
    一个用来表示内存中一大片非结构化区域的名字,对象都被分配在这。
  2. Queue(队列)
    一个 JavaScript runtime 包含了一个任务队列,该队列是由一系列待处理的任务组成。而每个任务都有相对应的函数。当栈为空时,就会从任务队列中取出一个任务,并处理之。该处理会调用与该任务相关联的一系列函数(因此会创建一个初始栈帧)。当该任务处理完毕后,栈就会再次为空。(Queue的特点是先进先出 First-in First-out (FIFO))。
    为了方便描述与理解,作出以下约定:
  • Stack栈为主线程
  • Queue队列为任务队列(等待调度到主线程执行)
    下图就是主线程和任务队列的示意图。
    mark

    事件和回调函数

    “任务队列”是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在”任务队列”中添加一个事件,表示相关的异步任务可以进入”执行栈”了。主线程读取”任务队列”,就是读取里面有哪些事件。
    “任务队列”中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入”任务队列”,等待主线程读取。
    所谓”回调函数”(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
    “任务队列”是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,”任务队列”上第一位的事件就自动进入主线程。但是,由于存在后文提到的”定时器”功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

    Event Loop

    主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
    mark
    上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在”任务队列”中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取”任务队列”,依次执行那些事件所对应的回调函数。
    执行栈中的代码(同步任务),总是在读取”任务队列”(异步任务)之前执行。请看下面这个例子。
    1
    2
    3
    4
    5
    var req = new XMLHttpRequest();
    req.open('GET', url);
    req.onload = function (){};
    req.onerror = function (){};
    req.send();

上面代码中的req.send方法是Ajax操作向服务器发送数据,它是一个异步任务,意味着只有当前脚本的所有代码执行完,系统才会去读取”任务队列”。所以,它与下面的写法等价。

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

也就是说,指定回调函数的部分(onload和onerror),在send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去读取”任务队列”。

定时器

包括setTimeout与setInterval两个方法。它们的第二个参数是指定其回调函数推迟每隔多少毫秒数后执行。
对于第二个参数有以下需要注意的地方:

  • 当第二个参数缺省时,默认为0;
  • 当指定的值小于4毫秒,则增加到4ms(4ms是HTML5标准指定的,对于2010年及之前的浏览器则是10ms);
    除了放置异步任务的事件,”任务队列”还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做”定时器”(timer)功能,也就是定时执行的代码。
    定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。以下主要讨论setTimeout()。
    setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。
    1
    2
    3
    console.log(1);
    setTimeout(function(){console.log(2);},1000);
    console.log(3);

上面代码的执行结果是1,3,2,因为setTimeout()将第二行推迟到1000毫秒之后执行。
如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。

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

上面代码的执行结果总是2,1,因为只有在执行完第二行以后,系统才会去执行”任务队列”中的回调函数。
总之,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在”任务队列”的尾部添加一个事件,因此要等到同步任务和”任务队列”现有的事件都处理完,才会得到执行。

需要注意的是,setTimeout()只是将事件插入了”任务队列”,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。
参考链接:
https://segmentfault.com/a/1190000004276027
http://www.ruanyifeng.com/blog/2014/10/event-loop.html

文章目录
  1. 1. 单线程你联想到什么?
  2. 2. 并发模式与Event Loop
  3. 3. javaScipt怎样实现并发模式呢?
  4. 4. 事件和回调函数
  5. 5. Event Loop
  6. 6. 定时器
|