前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JavaScript/Node.js 有协程吗?

JavaScript/Node.js 有协程吗?

作者头像
五月君
发布2021-07-15 13:10:45
3.9K0
发布2021-07-15 13:10:45
举报
文章被收录于专栏:Nodejs技术栈

从 Callback 到 Promise 的 .then().then()... 也是在不断尝试去解决异步编程带来的回调嵌套、错误管理等问题,Promise 进一步解决了这些问题,但是当异步链多了之后你会发现代码会变成这样 .then().then()... 由原来的横向变成了纵向的模式,仍就存在冗余的代码,基于我们大脑对事物的思考,我们更倾向于一种近乎 “同步” 的写法来表达我们的异步代码,在 ES6 规范中为我们提供了 Generator 函数进一步改善我们的代码编写方式。

Generator 中文翻译过来我们可以称呼它为 “生成器”,它拥有函数的执行权,知道什么时候暂停、什么时候执行,这里还有一个概念协程,有些地方也看到过一些提问:“JavaScript 中有协程吗?” “Node.js 中有协程吗?” 这些问题正是本文讨论的,本节着重从概念上让大家做一些了解,认识到协程在 JavaScript 是怎么样的存在。

进程 VS 线程 VS 协程?

在了解协程之前,先看进程、线程分别是什么,分享一个笔者之前写的 Node.js 进阶之进程与线程 文中结合 Node.js 列举了一些示例,也是从一些基础的层面来理解。

进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,进程是线程的容器(来自百科)。

我们启动一个服务、运行一个实例,就是开一个服务进程,例如 Java 里的 JVM 本身就是一个进程,Node.js 里通过 node app.js 开启一个服务进程,多进程就是进程的复制(fork),fork 出来的每个进程都拥有自己的独立空间地址、数据栈,一个进程无法访问另外一个进程里定义的变量、数据结构,只有建立了 IPC 通信,进程之间才可数据共享。

Mac 系统自带的监控工具 “活动监视器” 也可看到效果。

Node.js 中我们通过 Cluster 模块创建多进程时为什么要根据 CPU 核心数?创建更多不好吗?在一个 CPU 核心的任何时间内只能执行一个进程。因此,当你 CPU 核心数有限时,创建过多的进程,CPU 也是忙不过来的。

Node.js 通过单线程 + 事件循环解决了并发问题。而我们使用 Node.js 利用 Cluster 模块根据 CPU 核心数创建多进程解决的是并行问题,假设我有 4 CPU 每个 CPU 分别对应一个线程并行处理 A、B、C、D 不同的任务,线程之间互不抢占资源。

一句话总结:进程之间数据完全隔离、由操作系统调度,自动切换上下文信息,属系统层级的构造

线程

线程是操作系统能够进行运算调度的最小单位,首先我们要清楚线程是隶属于进程的,被包含于进程之中。一个线程只能隶属于一个进程,但是一个进程是可以拥有多个线程的。

同一块代码,可以根据系统 CPU 核心数启动多个进程,每个进程都有属于自己的独立运行空间,进程之间是不相互影响的。同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage),线程又有单线程和多线程之分,具有代表性的 JavaScript、Java 语言。

线程共享进程的资源,可以由系统调度运行,可以自动完成线程切换,也许你会听到多线程编程、并发问题,首先,并发指的某个时间点多个任务队列对应到同一个 CPU 上运行,在任一时间点内也只会有一个任务队列在 CPU 上执行,这时就产生排队了

为了解决这个问题,CPU 运行时间片会被分成多个 CPU 时间段,每个时间段给各个任务队列执行(对应多个线程),这样解决了一个任务如果造成阻塞,不会影响到其它的任务运行,同样线程是会自动切换的。

Node.js 是怎么解决的并发问题?Node.js 主线程是单线程的,核心通过事件循环,每次循环时取出任务队列中的可执行任务运行,没有多线程上下文切换,资源抢占问题,达到高并发成就。

一句话总结:线程之间大多数共享数据(各自的调用栈这些信息除外),由操作系统调用,自动切换上下文,系统层级的构造

协程

协程又称为微线程、纤程,英文 Coroutine。协程类似于线程,但是协程是协作式多任务的,而线程是抢占式多任务的。协程之间的调用不需要涉及任何系统调用,是语言层级的构造,可看作一种形式的控制流,有时候我们也会称它为用户态的轻量级线程。

协程一个特点是通过关键字 yield 调用其它协程,接下来每次协程被调用时,从协程上次 yield 返回的位置接着执行,这种通过 yield 协作转移执行权的操作,彼此没有调用者和被调用者的关系,是彼此平等对称的一种关系。

协程与线程两者的差异,可以看出 “同一时间如果有多个线程,但它们会都处于运行状态,线程是抢占式的,而协程同一时间运行的只有一个,其它的协程处于暂停状态,执行权由协程自己分配”。

协程也不是万能的,它需要配合异步 I/O 才能发挥最好的效果,对于操作系统而言是不知道协程的存在的,它只知道线程。需要注意,如果一个协程遇到了阻塞的 I/O 调用,这时会导致操作系统让线程阻塞,那么在这个线程上的其它协程也都会陷入阻塞。

一句话总结:协程共享数据,由程序控制完成上下文切换,语言层级的构造

JavaScript 有协程吗

之前知乎上有个问题 “Node.js 真的有协程吗?” 协程在很多语言中都支持,只是每个实现略有差异,下图来自维基百科展示了支持协程的编程语言,可以看到 JavaScript 在 ECMAScript 6 支持,ECMAScript 7 之后通过 await 支持,Node.js 做为 JavaScript 在服务端的运行时,只要你的 Node.js 版本对应支持,就是可以的

协程在 JavaScript 中的实现

生成器与协程

生成器(Generator)是协程的子集,也称为 “半协程”。差异在于,生成器只能把控制权交给它的调用者,完全协程有能力控制在它让位之后哪个协程立即接续它执行。在 JavaScript 里我们说的 Generator 函数就是 ES6 对协程的实现

JavaScript 是一个单线程的语言,只能保持一个调用栈。在异步操作的回调函数里,一旦出错原始的调用栈早已结束,引入协程之后每个任务可以保持自己的调用栈,这样解决的一大问题是出错误时可以找到原始的调用栈。

看下生成器函数与普通函数有什么区别?首先普通函数通过栈实现的,举个例子,调用时是 A() -> B() -> C() 入栈,最后是 C() -> B() -> A() 这样一个顺序最后进入的先出栈执行。

生成器函数看似和普通函数相似,其实内部执行机制是完全不同的,生成器函数在内部执行遇到 yield 会交出函数的执行权给其它协程(此处类似 CPU 中断),转而去执行别的任务,在将来一段时间后等到执行权返回(生成器还会把控制权交给它的调用者),程序再从暂停的地方继续执行

无堆栈协程

自 ES6 开始,通过 “Generator” 和 “yield” 表达式提供了无堆栈协程功能。

“无栈协程的秘密在于它们只能从顶级函数中挂起自己。对于其他所有函数,它们的数据都分配在被调用者堆栈上,因此从协程调用的所有函数必须在挂起协程之前完成。协程保留其状态所需的所有数据都在堆上动态分配。这通常需要几个局部变量和参数,其大小远小于预先分配的整个堆栈”。参考 coroutines-introduction

栈是一块连续的内存,能够从子函数产生的协程称为栈式,它们可以记住整个调用栈,这种也称为栈式协程。在 JavaScript 中我们只能从生成器函数内部暂停、恢复执行生成器函数。

下面示例 test1() 是生成器函数,但是 forEach 里面的匿名函数是一个普通的函数,就无法在内部使用 yield 关键字,运行时会抛出错误 “SyntaxError: Unexpected identifier”

代码语言:javascript
复制
function *test1() {
  console.log('execution start');
  
  ['A', 'B'].forEach(function(item) {
    yield item;
  })
}

生成器函数示例

例如,现在有两个生成器函数 test1()、test2(),还有 co 这个工具可以帮助我们自动的执行生成器函数。

代码语言:javascript
复制
const co = require('co');
function *test1() {
  console.log('execution 1');
  console.log(yield Promise.resolve(1));
  console.log('execution 2');
  console.log(yield Promise.resolve(2));
}

function *test2() {
  console.log('execution a');
  console.log(yield Promise.resolve('a'));
  console.log('execution b');
  console.log(yield Promise.resolve('b'));
}

co(test1);
co(test2);

看下运行结果:

  • 第一次程序执行 test1() 函数,先输出 'execution 1' 遇到 yield 语句程序的控制权转移。
  • 现在执行权转移到了 test2() 函数,执行代码输出 'execution a' 当遇到 yield 语句后交出程序的控制权。
  • 此时 test1() 函数收回执行权,恢复执行输出 '1' 继续往下执行输出 'execution 2' 当遇到 yield 语句再次交出执行权,依次类推。
代码语言:javascript
复制
execution 1
execution a
1
execution 2
a
execution b
2
b

总结

“JavaScript 有协程吗?” JavaScript 中是在 ES6 后基于生成器函数(Generator)实现的,生成器只能把程序的执行权还给它的调用者,这种方式我们称为 “半协程”,而完全的协程是任何函数都可让暂停的协程执行。

基于生成器函数这种写法,如果去掉 yield 关键字,与我们普通的函数是相似的,以一种同步的方式来表达,解决了回调嵌套的问题,另外我们还可以通过 try...catch 做错误捕获,只不过我们还需要借助 CO 这样的模块,让生成器函数自动执行,这个问题在 ES7 中已经得到了更好地解决,我们可以通过 async/await 轻松的实现。

Reference

  • https://en.wikipedia.org/wiki/Coroutine#Implementations_in_JavaScript
  • https://zhuanlan.zhihu.com/p/70256971
  • http://zhangchen915.com/index.php/archives/719/
  • https://es6.ruanyifeng.com/#docs/generator

往期回顾

- END -

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-06-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Nodejs技术栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 进程 VS 线程 VS 协程?
    • 进程
      • 线程
        • 协程
        • JavaScript 有协程吗
        • 协程在 JavaScript 中的实现
          • 生成器与协程
            • 无堆栈协程
              • 生成器函数示例
              • 总结
              • Reference
              • 往期回顾
              相关产品与服务
              容器服务
              腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档