重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
虽然我们生活在一个异步的世界里,但对于多数编程初学者来说,异步还是很陌生。学习一门编程语言,通常都是从同步流程开始的,即顺序、分支和循环。而异步流程是什么呢——开始一个异步调用,然后……就没有然后了。异步程序跑哪去了?
成都创新互联公司长期为1000多家客户提供的网站建设服务,团队从业经验10年,关注不同地域、不同群体,并针对不同对象提供差异化的产品和服务;打造开放共赢平台,与合作伙伴共同营造健康的互联网生态环境。为泽库企业提供专业的成都网站制作、成都做网站、外贸营销网站建设,泽库网站改版等技术服务。拥有十载丰富建站经验和众多成功案例,为您定制开发。异步程序会以某种异步的形式在运行着,比如多线程、异步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、模块化、设计模式、重构等概念。
相关阅读