文档章节

异步编程需要“意识”

边城
 边城
发布于 2018/02/23 09:10
字数 2430
阅读 164
收藏 0

虽然我们生活在一个异步的世界里,但对于多数编程初学者来说,异步还是很陌生。学习一门编程语言,通常都是从同步流程开始的,即顺序、分支和循环。而异步流程是什么呢——开始一个异步调用,然后……就没有然后了。异步程序跑哪去了?

异步程序会以某种异步的形式在运行着,比如多线程、异步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# 中是采用的 TaskTask<T>,相应的,使用了 Task.ContinueWithTask<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# 中,它会自动封装成 TaskTask<T>(所以 async 方法的类型需要声明为 TaskTask<T>)。

注意,注意,注意

尽管语言服务在异步程序同步化方面已经做了很多工作,但是仍然避免不了一些人为错误,比如忘记写 await 关键字。在强类型语言中编译器会检查得严格一些,但如果是在 JavaScript 中,忘记写 await 意味着原本应该取得一个值的语句,会取到一个 Promise。解释器不会对此质疑,但程序运行的结果会不正确。

小结

总的来说,异步编程并不是特别困难的事情。使用 async/await 语言特性甚至可以用类似编写同步代码的方法来编写异步代码。但语法糖终究是糖,要想把异步编程掌握得更好,还是需要去了解和熟悉异步、回调、Promise、模块化、设计模式、重构等概念。

相关阅读

© 著作权归作者所有

边城

边城

粉丝 91
博文 8
码字总数 18624
作品 1
绵阳
技术主管
私信 提问
加载中

评论(0)

为 PHP 开发人员介绍 Node.JS #1 : 事件驱动与意大利面

身为PHP开发人员,Node.js中最难弄清的部分就是异步。它是一种全新的代码方式。初步学习后,基于事件驱动的编程会为PHP开发人员创造更多的可能。我将为你解释他如何实现,不过首先来了解意大...

傅小黑
2013/01/30
4.3K
19
C#:异步编程和线程的使用(.NET 4.5 )

异步编程和线程处理是并发或并行编程非常重要的功能特征。为了实现异步编程,可使用线程也可以不用。将异步与线程同时讲,将有助于我们更好的理解它们的特征。 本文中涉及关键知识点 1. 异步...

葡萄城控件技术团队
2015/06/09
1.9K
1
Flutter | 金九银十招聘季,这些面试题祝你一臂之力

最近又开始了一年一度的「金九银十」,很多小伙伴希望趁着这个机会转阵 Flutter。 既然如此,总结一些面试题来送给大家。 先看看各大公司招聘的需求 首先来看看各个公司招聘的需求吧,毕竟面...

Flutter笔记
2019/09/08
0
0
使用Jscex改善JavaScript异步编程体验

JavaScript是互联网时代编程语言的霸主,统领浏览器至今已有许多年头,而这股风潮很可能随着HTML 5的兴起而愈演愈烈。如今JavaScript更是在Node.js的帮助下进军服务器编程领域。“单线程”和...

红薯
2011/08/02
1.4K
4
给new comer程序员的建议

first 程序员真的是吃青春饭吗?不是这样的! 在国内,很多人说程序员是吃青春饭的,一开始说过了30岁就得转行,后来又有人35岁是一道坎……看起来好像程序员真的和风尘女子一样,注定有朝一...

crossmix
2015/12/21
45
0

没有更多内容

加载失败,请刷新页面

加载更多

略谈分布式系统中的容器设计模式

本文作者:zytan_cocoa 略谈分布式系统中的容器设计模式 谭中意 2020/3/5 前言:云原生(Cloud Native)不仅仅是趋势,更是现在进行时,它是构建现代的,可弹性伸缩的,快速迭代的计算网络服...

百度开发者中心
03/11
7
0
OSChina 周三乱弹 —— 小姐姐的领带有点带歪了,请帮忙正一下

Osc乱弹歌单(2020)请戳(这里) 【今日歌曲】 @薛定谔的兄弟 :分享洛神有语创建的歌单「我喜欢的音乐」: 《アイタクテ -voice & piano-》- 和紗 手机党少年们想听歌,请使劲儿戳(这里) ...

小小编辑
今天
21
0
对象名称前的单下划线和双下划线是什么意思?

问题: Can someone please explain the exact meaning of having leading underscores before an object's name in Python? 有人可以解释一下在Python中对象名称前加下划线的确切含义吗? ......

技术盛宴
今天
29
0
Redis命令行参数大全

[TOC] Redis命令行参数大全 CLI工具的类型 可执行文件 作用 redis-server Redis Srver相关 redis-cli Redis命令行工具 redis-benchmark 基准测试工具 redis-check-aof AOF持久化文件检测工具...

我爱吃炒鸡
今天
20
0
RHEL8和CentOS8怎么重启网络

RHEL8和CentOS8怎么重启网络 本文主要讲解如何重启RHEL 8或者CentOS 8网络以及如何解决RHEL8和CentOS8系统的网络管理服务报错,当我们安装好RHEL 8或者 CentOS 8,重启启动网络时,会出现以下...

独钓渔
今天
42
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部