异步编程是JavaScript中重要的一块知识内容,但很多同学对此掌握的并不是很系统,所以接下来的章节我们会花大量的时间围绕异步编程进行讲解,我会做到尽量不放过任何一个知识死角,今天我们的任务就是先从宏观的角度理解什么是异步。
举例
跑步
设想你是一位体育老师,需要测验100位同学的400米成绩。
你下意识想到的第一个办法就是让100位同学一起 起跑,但这有个问题,当同学们到达终点时,你根本来不及掐表记录各位同学的成绩。
这个时候你想到了一个优化的办法,每次让一位同学起跑并等待他回到终点你记下成绩后再让下一位起跑,直到所有同学都跑完。
恭喜你,你已经掌握了同步阻塞模式。你设计了一个函数,传入参数是学生号和起跑时间,返回值是到达终点的时间。你调用该函数100次,就能完成这次测验任务。这个函数是同步的,因为只要你调用它,就能得到结果;这个函数也是阻塞的,因为你一旦调用它,就必须等待,直到它给你结果,不能去干其他事情。
如果你一边每隔10秒让一位同学起跑,直到所有同学出发完毕;另一边每有一个同学回到终点就记录成绩,直到所有同学都跑完。恭喜你,你已经掌握了异步非阻塞模式。你设计了两个函数,其中一个函数记录起跑时间和学生号,该函数你会主动调用100次;另一个函数记录到达时间和学生号,该函数是一个事件驱动的callback函数,当有同学到达终点时,你会被动调用。你主动调用的函数是异步的,因为你调用它,它并不会告诉你结果;这个函数也是非阻塞的,因为你一旦调用它,它就马上返回,你不用等待就可以再次调用它。但仅仅将这个函数调用100次,你并没有完成你的测验任务,你还需要被动等待调用另一个函数100次。
你马上就会意识到,异步非阻塞模式比同步阻塞模式的效率更高。那么,谁还会使用同步阻塞模式呢?不错,异步模式效率高,但更麻烦,你一边要记录起跑同学的数据,一边要记录到达同学的数据,而且同学们回到终点的次序与起跑的次序并不相同,所以你还要不停地在你的成绩册上查找学生号。
背景知识介绍
同步函数 vs 异步函数
同步函数:当一个函数是同步执行时,那么当该函数被调用时不会立即返回,直到该函数所要做的事情全都做完了才返回。
function run(学号, 起跑时间){
//到达终点时间...
}
run(001, 12:00);
//等待..
run(001, 12:06);
//等待..
run(001, 12:11);
//等待..
//....
run(001, 18:01);
//完成
异步函数:如果一个异步函数被调用时,该函数会立即返回 , 尽管该函数规定的操作任务还没有完成。
let time = 0;
for(var i = 1; i<=100; i++){
setTimeout(function(学号,起跑时间){
//到达终点时间...
}, time);
//下一位同学起跑时间
time += 10;
}
底层处理机制
当一个线程调用一个同步函数时(例如:该函数用于完成写文件任务),如果该函数没有立即完成规定的操作,则该操作会导致该调用线程的挂起(将CPU的使用权交给系统,让系统分配给其他线程使用),直到该同步函数规定的操作完成才返回,最终才能导致该调用线程被重新调度。
当一个线程调用的是一个异步函数(例如:该函数用于完成写文件任务),该函数会立即返回尽管其规定的任务还没有完成,这样线程就会执行异步函数的下一条语句,而不会被挂起。那么该异步函数所规定的工作是如何被完成的呢?当然是通过另外一个线程完成的了啊;那么新的线程是哪里来的呢?可能是在异步函数中新创建的一个线程也可能是系统中已经准备好的线程。
问题:一个调用了异步函数的线程如何与异步函数的执行结果同步呢?
为了解决这个问题,调用线程需要使用 “等待函数” 来确定该异步函数是否完成了规定的任务。
因此在 线程调用异步函数之后会挂起调用线程,一直等到异步函数执行完其所有的操作之后调用 “等待函数”。
同步调用 vs 异步调用
什么是线程?
要理解这个概念我们先的从进程讲起,进程就是一个程序的运行实例。启动一个应用程序的时候(假设浏览器),操作系统会为浏览器应用程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。
但我们的电脑又不可能只执行一个应用程序, 那么多个应用程序是如何协同在操作系统上并行执行的呢? 时间片轮转调度算法,每个进程被分配一个时间段,他就是进程允许运行的时间,称作它的时间片。如果在时间片结束时进程还在运行,那么CPU将被剥夺执行权并分配给另一个进程。
进程切换的代价太大了。
进程切换到另一个进程是需要一定时间的—保存和装入寄存器值及内存映像,更新各种表格和队列等。
所以这个时候就出现了线程的概念, 线程是操作系统能够运算调度的最小单元。
具体的做法是这样的假设我们有程序A, 那我会把程序A的运行逻辑 /任务 拆分多个分支, b c d, 这里b,c,d的执行是共享了A进程的上下文,也就是我们所说的线程。CPU在执行的时候仅仅切换线程的上下文,进程的切换的时间开销是远远大于线程上下文时间的开销。这样就让CPU的有效使用率得到提高。
[
](https://blog.csdn.net/qq_29229567/article/details/93978433)
多任运行过程的示意图如下:
在单线程方式下,一段代码调用另一段代码时,只能采用同步调用,必须等待这段代码执行完返回结果后,调用方才能继续往下执行。
有了多线程的支持,可以采用异步调用,调用方和被调方可以属于两个不同的线程,调用方启动被调方线程后,不等对方返回结果就继续执行后续代码。因此必须有一种机制让被调方有了结果时能通知调用方。常用的手段是回调、event 事件对象和消息。
回调:回调方式很简单:调用异步函数时在参数中放入一个函数地址,异步函数保存此地址,待有了结果后回调此函数便可以向调用方发出通知。
事件对象 : event 是 Windows 系统提供的一个常用同步对象,作用是在异步处理中对齐不同线程之间的步点。如果调用方暂时无事可做,可以找到 wait (等待) 函数等在那里,此时 event 处于 无信号 状态。当被调方出来结果之后,把 event 对象置于 发射信号 状态,wait 函数便自动结束等待,使调用方重新动作起来,从被调方取出处理结果。这种方式比回调方式要复杂一些,速度也相对较慢,但有很大的灵活性,可以搞出很多花样以适应比较复杂的处理系统。
消息:借助 Windows 消息发通知是个不错的选择,既简单又安全。程序中定义一个用户消息,并由调用方准备好消息处理例程。被调方出来结果之后立即向调用方发送此消息。
好说了这么多大家是否发现了同步编程比异步编程简单很多。这是因为,同步编程线性的思考是很简单的(调用A,调用A结束,调用B,调用B结束,然后继续,这是以事件处理的方式来思考)。 而在程序的开发过程中并不是线性的,往往我们需要在调用A的过程中去执行 文件 I/O 、发送网络请求等等一系列耗时的操作,所以异步编程是软件开发过程中的刚需,我们必须彻底的了解并且掌握它。
什么是进程什么是线程?
在解答这个问题之前,我们需要了解一下进程的概念,好多人容易把进程和线程的概念混淆,从而影响后续其他概念的理解,所以这里我就将这两个概念以及它们之间的关系一并为你讲解下。
不过,在介绍进程和线程之前,大家先要理解下什么是并行处理。
计算机中的并行处理就是同一时刻处理多个任务,比如我们要计算下面这三个表达式的值,并显示出结果。
A = 1+2
B = 20/5
C = 7*8
console.log(A,B,C);
在编写代码的时候,我们可以把这个过程拆分为四个任务:
- 任务 1 是计算 A=1+2;
- 任务 2 是计算 B=20/5;
- 任务 3 是计算 C=7*8;
- 任务 4 是显示最后计算的结果。
正常情况下程序可以使用单线程来处理,也就是分四步按照顺序分别执行这四个任务。
如果采用多线程,会怎么样呢?
我们只需分两步走:第一步,使用三个线程同时执行前三个任务;第二步,再执行第四个显示任务。
通过对比分析,你会发现用单线程执行需要四步,而使用多线程只需要两步。因此,使用并行处理能大大提升性能。
线程 VS 进程
多线程可以并行处理任务,但是线程是不能单独存在的,它是由进程来启动和管理的。
那什么又是进程呢?
一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。
为了让你更好地理解上述计算过程,我画了下面这张对比图:
单线程与多线程的进程对比图
从图中可以看到,线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率。
总结来说,进程和线程之间的关系有以下 4 个特点。
1. 进程中的任意一线程执行出错,都会导致整个进程的崩溃。
我们可以模拟以下场景做个简单的比喻:
进程==火车,线程==车厢
- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
- 线程之间共享进程中的数据。
- 线程之间共享进程中的数据(A车厢换到B车厢很容易)
- 进程之间的内容相互隔离
- 进程之间的内容相互隔离(一辆火车上的乘客很难换到另外一辆火车)
- 进程要比线程消耗更多的计算机资源
进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
- 当一个进程关闭之后,操作系统会回收进程所占用的内存。
当一个进程退出时,操作系统会回收该进程所申请的所有资源;
- 当一个进程关闭之后,操作系统会回收进程所占用的内存。