虽然我们生活在一个异步的世界里, 但对于多数编程初学者来说, 异步还是很陌生学习一门编程语言, 通常都是从同步流程开始的, 即顺序分支和循环而异步流程是什么呢开始一个异步调用, 然后就没有然后了异步程序跑哪去了?
异步程序会以某种异步的形式在运行着, 比如多线程异步 IO 等, 直到处理完成那如果需要处理结果怎么办? 给一个程序入口, 让它处理完当前过程之后, 把处理结果送到这个入口, 然后执行另一段程序俗称回调回调一般使用 callback 这个名称, 不过有时候我更喜欢使用 next , 因为它代表着下一个处理步骤
同步和异步的概念
现在我们接触到了一些概念, 比如同步和异步, 它们是什么?
这两个概念并不来源于编程语言, 而是来源于低层指令, 甚至更低层的电路它们是基于时序的两个概念, 其中, 步是指步调, 所以同步表示相同的步调, 而异步表示不同的步调当然这两个概念提升到程序这个级别的时候, 精确的意思与时钟无关, 但所表示的意义仍然未变
同步
举个生活中的例子来说明这个问题排除买票售票厅开了一个窗口, 有一队人在排队依次买票这个队伍中, 前面一个人往前走了一步, 后面的人才能往前走一步; 前面的人在等待, 后面的人就一定在等待那么在理想的情况下, 所有人可以同时向前迈步 OK, 大家步伐一致, 称为同步
这里把售票窗口看作是处理器, 每个人看作是等待执行的指令, 买票这个动作就是在执行指令它的特点是按步就班, 如果一个人买票时间过长(指令执行时间过长), 就会造成阻塞
异步(多线程)
现在买票的人渐渐多起来, 所以售票厅多开了几个窗口同时售票每个单独的队伍仍然保持着同步, 但不同的队伍之间, 步伐不再一致, 称为异步 A 队列售票很顺利, 队伍在有序快速的前进, 但 B 队列的某个顾客似乎在付费时遇到点麻烦, 花了很长的时候, 造成阻塞, 但这对 A 队列并不产生影响
这时候的售票厅可以看作是在以多线程的方式运行着异步程序从这个例子可以看到异步的两个特点: 其一, 两个异步流程之间相互独立, 它们相互不会阻塞(有个前提, 不需要等待共享资源的情况下); 其二, 异步程序内部仍然是同步的
异步(IO)
上面的例子比较符合多线程异步的情况那 IO 异步又是什么样呢?
年底了, M 在准备年终汇报的资料, 这可是个紧张的工作 (CPU), 要收集不少数据来写好些文案为了其中一份文案, M 需要车间的生产数据, 但跑一趟车间(IO) 可需要花不少时间, 所以他让 N 去车间收集数据, 自己则继续写其它方案, 同时等 N 把数据收集回来 (启动异步程序) 半天以后, N 带回了数据(插入事件消息),M 继续完成手上的文案(完成当前事件循环), 之后使用 N 带回来的数据开始撰写关于车间的报告(新的事件循环)
IO 的处理速度比 CPU 慢得多, 所以 IO 异步让 CPU 不必闲置着等待 IO 操作完成当 IO 操作完成之后, CPU 会适地使用 IO 操作结果继续工作
同步逻辑和异步逻辑
回到程序上来, 我们以一个函数的处理过程来描述同步和异步的处理方式
同步逻辑
那么, 同步处理过程是:
接受输入 处理 产生输出
用一段伪代码来描述就是
注: 本文中的伪代码比较接近 JavaScript 语法, 而有时候为了说明类型, 采用了 TypeScript 的类型申明语法
- function func(input) {
- do something with input
- return output
- }
这是标准的 IPO(Input-Process-Output) 处理
异步逻辑
而异步呢, 是:
接受输入 处理 启动下一步(如果有)
用伪代码来描述就是:
- function asyncFunc(input, next) {
- do something with input
- if (next is a entry) {
- next(output)
- }
- }
这个过程称为 IPN(Input-Process-Next)
注意到这里的 Next, 下一步, 只有一步这一步, 囊括了后续的若干步骤所以这一步, 只能是后续若干步骤封装出来一个模块入口, 或者说函数
因此, 模块化思想在异步思维中是一个非常关键的思想 很多初学者写代码喜欢像记流水账一样一句句往下写, 动不动就是成百上千行的函数, 这就是一种缺乏模块化思想的表现模块化思想需要训练, 分析代码的相关性, 提炼函数, 提取对象, 在具有一定经验之后还需要掌握模块细化的粒度平衡这不是一朝一夕之功, 不过我推荐看看设计模式和重构相关的书籍
异步开发工具(SDK 和语法层面的)
承诺(Promise)
再想想上面关于年终汇报的例子, M 请 N 去车间收集数据的时候, N 会说: 好的, 我很快就把数据带回来, 这是一种承诺基于这个承诺, M 才能安排后面撰写关于车间的汇报材料这个过程用伪代码来描述就是
- function collectData(): Promise {
- // N 去收集数据, 产生了一个承诺
- return new Promise(resolve => {
- collect data from workshop
- // 这个承诺最终会带来数据
- resolve(data)
- })
- }
- function writeWorkshopReport(data) {
- write report with data
- }
- // 收集数据的承诺兑现之后, 可将这个数据用于写报告
- collectData()
- .then(data => writeWorkshopReport(data))
以 JavaScript 为代表的一些语言 SDK 中使用了 Promise 不过 C# 中是采用的 Task 和 Task<T> , 相应的, 使用了 Task.ContinueWith 和
Task<T>.ContinueWith
来代替 Promise.then
异步逻辑同步化
上面提到了同步思维和异步思维在一个处理步骤中的区别如果跳出一个处理步骤, 从更大范围的处理流程来看, 异步与同步其实也没多大区别, 都是
输入 -->处理 -->产生输出 -->将输出用于下一步骤
, 唯一要注意的是需要等待异步处理产生的输出, 我们可以称之为 异步等待 由于我们可以一边进行异步等待(async wait, 简写 await), 一边做别的事情, 所以这个等待并不产生阻塞但是, 由于声明了这个等待, 编译器 / 解释器会将后面的代码自动放在等待完成之后调用, 这让异步代码写起来就像写同步代码一样
上面的例子使用异步等待的伪代码会像这样
- async
- function collectData() : Promise {
- collect data from workshop
- // 多数语言会把 async 函数的返回值封装成 Promise
- return data
- }
- function writeWorkshopReport(data) {
- write report with data
- }
- // await 只能用于声明为 async 的函数中
- async
- function main() {
- data = await collectData() writeWorkshopReport(data)
- }
- // 定义了异步 main 函数, 一定要记得调用, 不然它是不会执行的
- main()
像 C# 和 JavaScript 等语言都从语法层面规定了 await 必须用在声明为 async 的函数中, 这就从编译 / 解释的层面限定了 await 的用途, 只要使用了 await , 那它所处的就一定是一个异步上下文而 async 也要求编译器 / 解释器对其返回值进行一些自动处理, 比如在 JavaScript 中, 其返回值如果不是 Promise 对象, 它会自动封装成一个 Promise 对象; 而在 C# 中, 它会自动封装成 Task 或 Task<T> (所以 async 方法的类型需要声明为 Task 或 Task<T> )
注意, 注意, 注意
尽管语言服务在异步程序同步化方面已经做了很多工作, 但是仍然避免不了一些人为错误, 比如忘记写 await 关键字在强类型语言中编译器会检查得严格一些, 但如果是在 JavaScript 中, 忘记写 await 意味着原本应该取得一个值的语句, 会取到一个 Promise 解释器不会对此质疑, 但程序运行的结果会不正确
小结
总的来说, 异步编程并不是特别困难的事情使用 async/await 语言特性甚至可以用类似编写同步代码的方法来编写异步代码但语法糖终究是糖, 要想把异步编程掌握得更好, 还是需要去了解和熟悉异步回调 Promise 模块化设计模式重构等概念
来源: http://www.tuicool.com/articles/yIVnuyu