聊聊“异步”
在我们编程的时候,经常会遇到一个概念——异步,诸如异步通信,异步线程,异步代码,异步调用,异步编程等等,那么
什么是异步呢?
为什么要异步?
异步的典型场景是什么?
如何使用异步呢?
......
异步——通信?
老码农初识异步是从单片机的串行通信开始的。串行通信,是指通信双方按位进行,遵守时序的一种通信方式。串行通信有两种类型,一种是同步通信,另一种就是异步通信。
同步通信的特点是要求发送时钟和接收时钟保持严格的同步,异步通信的发送端和接收端可以由各自的时钟来控制数据的发送和接收,这两个时钟源彼此独立,互不同步。异步通信中的接收方并不知道数据什么时候会到达,发送方发送的时间间隔可以不均匀,接收方是在数据的起始位和停止位的帮助下实现信息同步的。简单的说,异步是扔出去一段数据,对方靠着内容前后所检查到的特殊性发现了它,把这个内容存下来;而同步通信是对方在时刻等着发送方发号施令,发送方告诉对方要发送了,然后双方一拍即合。
从通信效率来看,同步通信效率高,异步通信效率较低。但从实现方式来看,同步通信较复杂,异步通信相对简单,计算机的接口大多是异步的。
进一步,对通信网络而言,同步网一般是指网络内所有电信设备的时钟(或载波)提供同步控制信号,使它们的频率工作在共同速率(或频率)上的支撑网。同步网可分为准同步网和同步网两类,由具有相同标称频率的不同基准时钟互相比对的同步网称为准同步网,由单一基准时钟控制的称为同步网。我国和大多数国家采用分级主从同步法,国家间采用准同步法。异步网络不需要时间同步,可以在任何节点完成逐分组的转发,这种分组的不可预测和不规则机制增加了网络的阻塞率。然而,异步网络具有同步网络所不具备的低成本、低复杂度、高健壮性和高灵活性,通过合理设计交换的结构和协议,也可达到良好的交换性能。
关于通信网乃至TMN,太容易给人带来回忆了。跳出涌现的往事,对程序员而言,异步的概念有了相当程度的延伸。
异步——编程?
编程中的同步与异步往往是指两个对象之间的调用关系:
同步调用:调用者发出一个调用时,在没有得到结果之前,该调用不返回。一旦调用返回,就得到返回值了,也就是由调用者主动等待这个调用的结果。
异步调用:调用者发出一个调用之后,这个调用就直接返回了,没有返回结果,也就是当一个异步调用发出后,调用者不会立刻得到结果。而是在调用发出之后,被调用者通过状态、消息等来通知调用者,或通过回调函数处理这个调用。
对于异步编程而言,和软件系统的架构设计有类似的地方,大体上,可以分为面向CPU的异步编程和面向IO的异步编程这两种方式。
面向CPU 的异步
我们常见的多线程中就会经常遇到面向CPU的异步编程。线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。同步线程是指两个线程的运行是相关的,其中一个线程可能要阻塞等待另外一个线程的运行。异步线程是两个线程毫不相关,自己运行自己的。
这里也经常遇到另外的两个概念——阻塞和非阻塞,在多线程编程中,主要是指线程是否需要等待。阻塞调用指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用是指在不能立刻得到结果之前,该调用不会阻塞当前线程。
在Android上编程的时候,UI主线程和子线程的交互几乎是不可或缺的。在服务器侧,同样如此,SpringBoot 中配置异步线程池的简单示例如下:
//启动异步
@EnableAsync
//配置类
@Configuration
class ThreadsPoolConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
//创建建线程池
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//初始的线程数量
executor.setCorePoolSize(INI_THREAD_NUM);
//最大的线程数量
executor.setMaxPoolSize(MAX_THREAD_NUM);
//队列的最大容量
executor.setQueueCapacity(MAX_QUEUE_CAP);
//存活时间
executor.setKeepAliveSeconds(ALIVE_TIME);
//线程池的饱和策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//关闭线程池时是否等待任务完成
executor.setWaitForTasksToCompleteOnShutdown(true);
//等待终止的时间
executor.setAwaitTerminationSeconds(WAIT_TERMINATE_TIME);
return executor;
}
}
面向IO的异步
面向IO的操作,是异步编程的应用场所之一。在通过IO访问数据的方式,同步编程需要主动读写数据,在读写数据的过程中还是可能会遇到阻塞;异步编程只需要I/O操作完成的通知,并不主动读写数据,而是由操作系统内核完成数据的读写。
在《Unix网络编程》第二卷中,提到了5种IO模型:
前四种io模型为同步io模型,只有异步io模型与posix定义的io相匹配。异步IO在用户进程触发I/O操作以后就立即返回,继续开始做自己的事情,而当I/O操作已经完成的时候会得到I/O完成的通知。异步IO的执行者是内核线程,内核线程将数据从内核态拷贝到用户态,所以没有阻塞。
Linux2.6以后引入了AIO,主流的IO机制可能是EPOLL,一种性能卓越且编程简单的异步IO机制,在Nginx的配置中就可以看到它的身影。
网络编程中的异步
网络编程是一种特殊的IO操作,socket 编程中同样存在着同步和异步调用。网络编程的目的是网络通信,而网络通信的传输协议无外乎那八字的四原则:
组装
复用
纠错
流控
在Python中,可以asyncio来实现网络通信中的异步编程,asyncio库包含异步IO、事件循环、协程、task等内容,事件循环是asyncio提供的核心运行机制。
从分布式系统的角度来看,消息队列提供了异步通信的能力,借助消息队列,多个模块间可以灵活的进行消息传递,这里的异步通信就是指消息生产者可以将消息放入队列中,而不等待结果返回,由消息队列负责投递消息给消费者。在系统设计中,我们可以通过消息队列来进行模块间的通信。
太多的概念了,其实都是为了试图理解“异步”这一核心概念,还是举个例子吧。
DuerOS 中的异步推送
DBP开放平台向开发者开放了技能内异步推送的机制,技能内推送意味着开发者能够在用户的会话周期内,异步调用推送接口向设备端推送相关内容或协议指令。典型的应用场景,包括银行类耗时较长的操作处理,对用户的异步通知等等。
目前DBP平台提供了两大类的异步推送,分别为文本和BOT协议。文本又分为纯文本,使用该类型将在设备端底部展示一个通知,同时内容为文本内容;另一种是TTS,设备端将用语音播报相关的TTS;BOT协议提供了更丰富的设备端内容展示的情景。
权限申请
使用服务之前需要先在DBP开放平台申请该服务的权限:
编辑技能->配置服务,在服务权限配置下可以看到“技能内异步推送”,点“申请”,申请信息会加到审核列表里,待运营人员审核通过后,服务的状态将变成“已通过”,服务的权限即申请完成。若想下掉该权限,点击“下线”即可下线服务。
使用模板
文本模板
依次点击"编辑技能->推送服务→内容模板",进入文本模板页,该页由DBP提供了部分通用的系统模板,开发者只需在调用相关接口时更改相关参数即可:
通配参数在模板里用%%包含的字符串表示,同时该页面提供了渲染模板的测试工具,填入相应的模板ID、参数的键值对,点击生成后就能得到最终渲染完的文本内容:
BOT协议模板
点击“BOT协议”导航,进入BOT协议模板列表页,这里列出了DBP当前支持使用的BOT协议模板:
如上图,目前DBP提供了AudioPlayer.Play指令模板,使用该指令时,通过推送接口将会让设备端调起AudioPlayer并播放指定的音频。
点击AudioPlayer.Play链接,进入详情页,详情页里展示了该指令支持的字段、字段类型、可选、是否可自定义以及示例等信息,推送接口将会根据这些定义项进行数据校验,开发者在使用时不要传错数据:
对于部分模板,DBP提供了可自定义的字段,可以设置自定义字段的键与类型,提交审核通过后,就可以使用了,目前支持的类型分别为STRING,INT, ARRAY, OBJECT, BOOLEAN,所填的字段都是必须传的,推送接口会校验相应的字段与类型:
DEBUG 调试
BOT协议模板未审核通过前,可以先debug,debug时需要用户绑定自己的设备SN,设备SN在设备的底部,每个技能最多只能绑定5个设备:
调用推送接口
推送接口地址为:https://xiaodu.baidu.com/saiya/v1/notification/reprompt
,method为POST,需要设置Content-Type为application/json:
$ curl --location --request POST 'https://xiaodu.baidu.com/saiya/v1/notification/reprompt' \
--header 'Authorization: bearer {{apiAccessToken}}' \
--header 'Content-Type: application/json' \
--data-raw '{
"dialogRequestId": {{dialogRequestId}},
"notificationType": "plainText",
"templateId": "1",
"debug": 1,
"templateParams": {
"userName": "张三",
"botName": "测试技能"
}
}'
其中header里要将{{apiAccessToken}}替换成实际的apiAccessToken,该值从BOT request里获取,在context.System.apiAccessToken
里。POST的body为json格式,其中:
dialogRequestId: 必选,从request里获取
request.dialogRequestId
notificationType: 必选,值分别为,plainText-纯文本类型,将在有屏设备底部以通知方式呈现;plainTts-将以语音方式播报;botProtocol-BOT协议类型
templateId: 必选,当notificationType为plainText和plainTts时,这个值就是内容模板里的模板ID值,为数值;当为botProtocol时,就是BOT协议里的指令名称,如AudioPlayer.Play
debug: 可选,为1时,工作在debug模式,指令只能往指定设备推送,为0或不填时工作在线上模式
templateParams: 可选,当notificationType为plainText和plainTts时,内容为文本模板里参数的键值对;当为botProtocol时,值为具体的BOT协议内容。
例如,一个bot协议的推送请求如下:
curl --location --request POST 'https://xiaodu.baidu.com/saiya/v1/notification/reprompt' \
--header 'Authorization: bearer {{apiAccessToken}}' \
--header 'Content-Type: application/json' \
--data-raw '{
"dialogRequestId": {{dialogRequestId}},
"notificationType": "botProtocol",
"templateId": "AudioPlayer.Play",
"debug": 1,
"templateParams": {
"type": "AudioPlayer.Play",
"playBehavior": "REPLACE_ALL",
"audioItem": {
"stream": {
"url": {{音频URL}},
"token": "7de36bb44852ae287028ba830565a6ef"
},
"playInfo": {
"content": {
"title": "音频测试",
"titleSubtext1": "音频"
}
}
}
}
}'
小结
许多工作和技术上的争执源自概念的混淆,技术上的概念不同于文学作品——“每个人眼中都有一个自己的哈姆雷特”。讨论问题的基础好像应该是,澄清概念和明确问题的领域边界。
异步是一个常见的概念,但在不同的场景中有着不同的含义,本文梳理一下相关内容,试图可以澄清一些。