异步流程控制浅谈

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链式调用更为合适——利用函数的组合来把整个流程表达清楚

异步流程控制

正常的编程语言的流程控制,和异步流程控制稍有差别。我们讨论异步流程控制必须在概览整个编程面貌的前提下去讨论,也就是,异步流程控制代码必然是建立在同步代码上的。上文有说过,编程语言的流程控制共有三个分类:顺序执行,条件执行,循环执行。而异步流程控制,我们需要考虑的是:串行执行、并行执行、错误处理

参考链接

posted @ 2021-03-12 12:00  西河  阅读(10)  评论(0编辑  收藏  举报