异步流程控制浅谈
bash脚本语言,基本上每个命令都是需要不定时等待的,所以是一种天然的异步流程控制语言
“我们要寻找的就是如何组织我们的代码,来让其更加容易,极少冗余的表述我们的思维过程,同时这个过程本身又是容易被不断延展的,我称之为编织代码。”
表面同步,内在异步的模型,才是高并发编程和高效率编程的最终方案
概念
流程
由两个及以上的业务步骤,完成一个完整的业务行为的过程,可称之为流程;注意是两个及以上的业务步骤
简言之流程就是过程节点及执行方式有序组成的过程。
异步
异步双方不需要共同的时钟,也就是接收方不知道发送方什么时候发送
线程
线程是进程中某个单一顺序的控制流。也被称为轻量进程(lightweight processes).计算机科学术语,指运行中的程序的调度单位
当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者
流程控制
在声明式的编程语言中,流程控制指令是指会改变程序运行顺序的指令,可能是运行不同位置的指令,或是在二段(或多段)程序中选择一个运行。
目的:规定程序执行的顺序
流程控制可分为三类:
- 第一个是顺序执行。这个非常简单,就是先执行第一行再执行第二行……这样依次从上往下执行。
- 第二个是选择执行。也就是说,有些代码可以跳过不执行,有选择地执行某些代码。
- 第三个是循环执行。也就是说,有些代码会反复执行。
代码
定义一个异步的加法函数
// 使用promise
function asyncAdd(a, b) {
return new Promise((resolve, reject) => {
if (typeof a !== 'number' || typeof b !== 'number') {
return reject('arguments type wrong!')
}
setTimeout(() => {
resolve(a + b)
}, 50)
})
}
// 使用callback
function callbackAdd(a, b, cb) {
if (typeof cb !== 'function') {
throw new Error('arguments:cb must be function')
}
if (typeof a !== 'number' || typeof b !== 'number') {
return cb('arguments type wrong!')
}
setTimeout(() => {
cb(null, a + b)
}, 50)
}
要求:使用asyncAdd方法,执行从1…100的循环操作
方案0:传统的callback
语法
console.time('callback 100')
callbackAdd(1, 2, (err, result) => {
if (err) {
throw new Error(err)
}
callbackAdd(result, 3, (err, result) => {
if (err) {
throw new Error(err)
}
callbackAdd(result, 4, (err, result) => {
// do 99 times callback...
callbackAdd(result, 100, (err, result) => {
if (err) {
throw new Error(err)
}
console.log('async sum 1..100', result)
console.timeEnd('callback 100')
})
})
})
})
方案1:Promise
语法
console.log('start calc for 100 times iterable')
console.time('process 100')
let p = Promise.resolve(0)
for (let i = 1; i <= 100; i++) {
p = p.then(val => asyncAdd(val, i))
}
p.then(val => {
console.log('async sum 1..100', val)
console.timeEnd('process 100')
})
输出如下
start calc for 100 times iterable
async sum 1..100 5050
process 100: 6.239s
方案1.1:Promise.all
并行计算写法
console.log('start calc for 10 times iterable')
console.time('process 10')
const ps = Array.from(Array(10), () => Promise.resolve(0))
for (let i = 1; i <= 10; i++) {
for (let j = 0; j < 10; j++) {
ps[j] = ps[j].then(val => asyncAdd(val, 10 * j + i))
}
}
Promise.all(ps).then(list => {
const sum = list.reduce((result, cur) => {
result += cur
return result
}, 0)
console.log('async sum 1..100', sum)
console.timeEnd('process 10')
})
输出如下:
start calc for 10 times iterable
async sum 1..100 5050
process 10: 612.766ms
方案2:async/await
语法
async function asyncAddCalc() {
console.log('start async iterable')
console.time('async 100')
let result = 0
let i = 1
while (i <= 100) {
result = await asyncAdd(result, i++)
}
console.log('async sum 1..100', result)
console.timeEnd('async 100')
}
asyncAddCalc()
输出如下:
start async iterable
async sum 1..100 5050
async 100: 6.225s
方案2.1:async/await
语法并行计算写法
async function asyncAddCalc() {
console.log('start async iterable')
console.time('async 10')
let result = 0
const ps = Array.from(Array(10), () => null)
const num10 = Array.from(Array(10), (_, idx) => idx + 1)
for (let i = 0; i < 10; i++) {
ps[i] = num10.reduce(async (p, val) => {
const pre = await p
const cur = 10 * i + val
return await asyncAdd(pre, cur)
}, 0)
}
const arr = await Promise.all(ps)
for (const num of arr) {
result += num
}
console.log('async sum 1..100', result)
console.timeEnd('async 10')
}
asyncAddCalc()
输出如下:
start async iterable
async sum 1..100 5050
async 10: 613.137ms
方案3:for await of
语法
const asyncIterable = {
[Symbol.asyncIterator]() {
return {
i: 0,
result: 0,
next() {
if (this.i <= 100) {
return asyncAdd(this.result, this.i).then(val => {
this.i++
this.result = val
return {
value: this.result,
done: false
}
})
}
return Promise.resolve({ done: true })
}
}
}
}
async function* asyncAddGenerator() {
let i = 0
let result = 0
while (i <= 100) {
result = await asyncAdd(result, i++)
yield result
}
}
async function awaitIterableAdd() {
console.log('start foawaititerable')
console.time('foawait')
let result = 0
// asyncAddGenerator() 也可替换成 asyncIterable
for await (let sum of asyncAddGenerator()) {
result = sum
}
console.log('async sum 1..100', result)
console.timeEnd('foawait')
}
awaitIterableAdd()
输出为:
start foawait iterable
async sum 1..100 5050
foawait: 6.298s
由上面的解决方案,我们可以得出如下数据:
方案 | 可读性 | 是否并行 |
---|---|---|
callback | 差,基本无法使用 | 可以 |
Promise | 良好 | 可以 |
async/await | 优秀 | 可以 |
for…await…of | 优秀 | 不可以 |
解释: |
- 在上面的例子中,如果用callback来解答,需要写100个级联的回调函数,完全是不可行的。
for…await…of
语法需要使用实现了异步迭代器协议的对象,异步迭代器必然是串行执行的,所以是不能并行执行的。
因此,异步流程控制,还是采用Promise
,async/await
的语法比较好些。那么,我们试着讨论下它们的优缺点吧。
语法 | 优点 | 缺点 |
---|---|---|
Promise | 心智负担小,链式调用,提供了优雅的API用于处理并行的状况,允许捕获处理某个流程的错误后,继续链式调用执行下面的流程 | 很难在后面的异步流程中访问中间值,错误捕获不够完美,调试then 里面的函数不方便 |
async/await | 可以访问中间值,可以使用try...catch 完美的捕获同步、异步错误。调试方便 | 并行执行代码心智负担大,没有链式调用的写法优雅 |
注意:
Promise
是一个对象,而async/await
则是关键字,所以,Promise
的API可以设置很细节。然而async/await
却不行。这表现在代码层面上,我们会发现,使用Promise
书写异步代码很优雅,心智负担小。
async/await
只适合命令式的调用,适合过程表达。要梳理流程的话,还是Promise的then链式调用更为合适——利用函数的组合来把整个流程表达清楚
异步流程控制
正常的编程语言的流程控制,和异步流程控制稍有差别。我们讨论异步流程控制必须在概览整个编程面貌的前提下去讨论,也就是,异步流程控制代码必然是建立在同步代码上的。上文有说过,编程语言的流程控制共有三个分类:顺序执行,条件执行,循环执行。而异步流程控制,我们需要考虑的是:串行执行、并行执行、错误处理。