当前位置: 首页 > 科技观察

JavaScript异步编程指南——关于协程的一些思考

时间:2023-03-12 02:31:47 科技观察

本文转载自微信公众号《五月君》,作者吴越君。转载本文请联系MayJun公众号。从Callback到Promise的.then().then()...也在不断尝试解决异步编程带来的回调嵌套和错误管理等问题。Promise进一步解决了这些问题,但是当异步链比较多的时候你会发现代码会变成这样.then().then()...从原来的横向模式到纵向模式,还是有多余的代码。基于我们大脑对事物的思考,我们更倾向于用一种近乎“同步”的写法来表达我们的异步代码,在ES6规范中为我们提供了一个Generator功能来进一步完善我们的代码写法。Generator从中文翻译过来,我们可以称之为“发电机”。它有执行功能的权利,知道什么时候暂停,什么时候执行。还有一个协程的概念,在一些地方看到了一些问题:“JavaScript有协程吗?”“Node.js有协程吗?”本文将讨论这些问题。本节着重于理解协程的概念以及协程在JavaScript中的存在方式。进程VS线程VS协程?在了解协程之前,我们先来看看进程和线程分别是什么。分享一个作者之前写的Node.js进阶流程和线程。一些例子是结合Node.js列出的,也是来自一些基础层面的理解。Process进程(Process)是计算机中程序在一定数据集上的运行活动。它是系统资源分配和调度的基本单位,是操作系统结构的基础。进程是线程容器(来自百科全书)。当我们启动一个服务并运行一个实例时,我们打开一个服务进程。比如Java中的JVM,它本身就是一个进程。在Node.js中,通过nodeapp.js启动一个服务进程。多进程就是进程的拷贝(fork),fork出来的每个进程都有自己独立的空间地址和数据栈。一个进程不能访问另一个进程中定义的变量和数据结构。只有建立了IPC通信,进程之间才能共享数据。Mac系统自带的监控工具“ActivityMonitor”也能看到效果。在Node.js中,为什么我们通过Cluster模块创建多进程时需要根据CPU核数来计算?创造更多不好吗?一个CPU核心上任何时候只能执行一个进程。因此,当你的CPU核数有限的时候,如果你创建的进程太多,CPU就会忙不过来。Node.js用单线程+事件循环解决了并发问题。而我们在Node.js中使用Cluster模块根据CPU核数创建多个进程来解决并行问题。假设我有4个CPU,每个CPU对应一个线程并行处理A、B、C、D的不同任务。线程之间不要相互抢占资源。一句话概括:进程间数据完全隔离,由操作系统调度,上下文信息自动切换,属于系统级结构。线程线程是操作系统能够进行计算调度的最小单位。首先我们要明白,线程属于进程,包含在进程中。一个线程只能属于一个进程,但一个进程可以有多个线程。同一段代码可以根据系统的CPU核数启动多个进程。每个进程都有自己独立的运行空间,进程之间互不影响。同一个进程中的多个线程会共享进程中所有的系统资源,比如虚拟地址空间、文件描述符、信号处理等。但是同一个进程中的多个线程都有自己的调用栈(callstack),各自的寄存器上下文(registercontext),各自的线程本地存储(thread-localstorage),线程分为单线程和多线程-线程。代表JavaScript、Java语言。线程共享进程资源,可以被系统调度运行,可以自动完成线程切换。也许你会听说多线程编程和并发问题。首先,并发指的是多个任务队列在某个时间点对应同一个CPU,在任意时刻CPU上只会有一个任务队列在执行,此时就会发生排队。为了解决这个问题,会把CPU运行时间片分成多个CPU时间段,每个时间段针对每个任务队列(对应多个线程)执行,这样如果一个任务造成阻塞,就不会影响其他任务运行,同一个线程会自动切换。Node.js是如何解决并发问题的?Node.js的主线程是单线程的。核心通过事件循环,每次执行循环时,将任务队列中的可执行任务取出运行。没有多线程上下文切换,资源抢占,实现了高并发的成就。一句话概括:线程间共享的大部分数据(各自的调用栈信息除外),由操作系统调用,自动上下文切换,系统级构建。Coroutine协程也叫微线程、纤程,英文Coroutine。协程类似于线程,但协程是协作式多任务,而线程是抢占式多任务。协程之间的调用不需要涉及任何系统调用。它们是语言级别的结构,可以被视为一种控制流形式。有时我们也称它们为用户态的轻量级线程。协程的一个特点就是通过关键字yield来调用其他协程,然后每次调用协程都会从协程上次yield返回的位置继续执行。这种通过yield合作来转移执行权的操作,并没有相互调用调用者和被调用者之间的关系是一种平等对称的关系。协程和线程的区别可以看出“如果同时有多个线程,它们都会处于运行状态,线程是抢占式的,同时只有一个协程在运行,其他协程处于运行状态,挂起状态,执行权由协程自己分配。协程不是万能的。需要配合异步I/O才能发挥最佳效果。对于操作系统来说,它并不知道协程的存在,它只知道线程。需要注意的是,如果一个协程遇到阻塞的I/O调用,会导致操作系统阻塞线程,那么这个线程上的其他协程也会被阻塞。一句话总结:协程共享数据,上下文切换由程序控制完成,语言级构建。JavaScript有协程吗?之前Quora上有个问题,“Node.js真的有协程吗?”许多语言都支持协程,但每种语言的实现都略有不同。下图来自维基百科,显示了对协程的支持。可以看到ECMAScript6支持JavaScript,通过await支持ECMAScript7。在服务器端使用Node.js作为JavaScript时,只要你的Node.js版本支持,都是可以的。JavaScript中协程的实现生成器和协程生成器(Generator)是协程的子集,也称为“半协程”。不同之处在于生成器只能将控制权交给它的调用者,而完整的协程可以控制哪个协程在让位后立即恢复执行。我们在JavaScript中所说的Generator函数是ES6对协程的实现。JavaScript是一种单线程语言,只能维护一个调用栈。在异步操作的回调函数中,一旦出错,原来的调用栈就已经结束了。引入协程后,每个任务都可以保留自己的调用栈。这样就解决了一个很大的问题,就是出错的时候能找到原来的调用栈。让我们看看生成器函数和普通函数有什么区别?首先通过栈实现一个普通的函数。比如调用的时候,A()->B()->C()入栈,最后C()->B()->A()就是最后进入,最后弹出的序列首先是堆栈。生成器函数看似与普通函数相似,但内部执行机制却完全不同。Generator函数在内部执行遇到yield时会将函数的执行权交给其他协程(类似于这里的CPU中断),然后去执行其他任务,等待一段时间后执行权返回未来(生成器也将控制权交给它的调用者),程序将从暂停的地方继续执行。Stacklesscoroutines自ES6以来,stacklesscoroutines已经通过“Generator”和“yield”表达式提供。“stackless协程的秘诀在于它们只能将自己从顶层函数挂起。对于所有其他函数,它们的数据都分配在被调用者堆栈上,因此从协程调用的所有函数都必须在协程完成之前挂起。协程保持其状态所需的所有数据都在堆上动态分配。这通常需要几个局部变量和参数,其大小比预分配的整个堆栈小得多。参考coroutines-introduction栈是一个连续的内存。可以从子函数生成的协程称为堆栈。他们可以记住整个调用堆栈,也称为堆栈协程。在JavaScript中,我们只能在生成器函数中暂停和恢复生成器函数的执行。下面的例子test1()是生成器函数,但是forEach中的匿名函数是普通函数,所以里面不能使用yield关键字,运行时会抛出“SyntaxError:Unexpectedidentifier”的错误function*test1(){console.log('executionstart');['A','B'].forEach(function(item){yielditem;})}生成器函数示例比如现在有两个生成器函数test1(),test2(),而co是一个可以帮助我们自动执行生成器函数的工具。constco=require('co');function*test1(){console.log('execution1');console.log(yieldPromise.resolve(1));console.log('execution2');console.log(yieldPromise.resolve(2));}function*test2(){console.log('executiona');console.log(yieldPromise.resolve('a'));console.log('executionb');console.log(yieldPromise.resolve('b'));}co(test1);co(test2);看运行结果:程序第一次执行test1()函数,先输出'execution1',控制当遇到yield语句时遇到程序权转移。现在执行权交给了test2()函数,执行代码在遇到yield语句时输出'executiona',移交程序的控制权。此时test1()函数收回执行权,恢复执行输出'1'并在遇到yield语句再次交出执行权时继续执行输出'execution2',以此类推。execution1executiona1execution2aexecutionb2bSummary"DoesJavaScripthavecoroutines?"JavaScript在ES6之后是基于生成器函数(Generator)实现的,生成器只能将程序的执行权返回给它的调用者。Coroutine”,而全协程是任何可以导致协程挂起执行的函数。基于生成器函数的写法,如果去掉yield关键字,就和我们普通的函数类似。它用同步来表示方式,解决了回调嵌套的问题。另外,我们也可以使用try...catch做错误捕获,但是我们还是需要使用CO之类的模块让generator函数自动执行。这个问题在中已经得到较好的解决ES7,我们可以通过async/await轻松实现。参考https://en.wikipedia.org/wiki/Coroutine#Implementations_in_JavaScripthttps://zhuanlan.zhihu.com/p/70256971http://zhangchen915.com/index.php/archives/719/https://es6.ruanyifeng。com/#docs/生成器