9月底,Swift 5.5正式发布,这是一个相对比较重要的版本,因为它引入了一个新的特性,async/await。
我一直非常关注这个特性,因为几乎在其它所有端都已经全面支持async/await这个特性,这个特性对于异步编码的代码简洁性及普及,具有非常高的价值。
可能还有些人并不是非常清楚async/await是用来做什么的,我简要说明下。
要说async/await,可能要从JavaScript说起。
和Java这种语言不太一样,JavaScript是单线程的,所以它只能设计成异步的。异步的代码和同步的代码在思维上截然不同。举例说明:
在Java下的同步模式
//假设executeSomeBigCal是一个非常耗时的操作,在Java中,我们可以将线程卡在这等待它执行完再返回
dobule result = executeSomeBigCal();
//拿到结果后,再恢复运行,线程得以继续运行
System.out.print("结果是:" + result);
由于JavaScript是单线程的,肯定没法像Java这样,这样就挂了。于是JavaScript是事件回调模式。什么概念呢?
在JavaScript下的实现
executeSomeBigCal(function(result){
console.log("结果是:" + result)
});
可以看到,它与Java完全不同,它给executeSomeBigCal传递了一个函数,而不是等待executeSomeBigCal返回结果。
在JavaScript中,当executeSomeBigCal执行完成后,再回调执行传入的函数。这样几乎不会阻塞,都是一个执行完,回调下一步,而不是等待一个执行完,再执行下一步。
这就是同步编程与异步编码的根本性不同。由于没有阻塞线程,异步模式下的性能远优于同步阻塞的模式,所以在Java阻塞式的编程中,基本都会遇到一个问题就是大并发下系统的线程不够的问题。
而在异步模式下,由于不阻塞线程,没有这个问题。
前些年非常火的NodeJS之所以性能非常好,原因在于它也是基于JavaScript的,也是异步模式。
异步模式下由于不阻塞线程,虽然性能较好,但一个业务通常不可能只包含一两个过程,任何一个业务可能包含非常多个过程,这就会形成一种非常不好的代码风格。
executeSomeBigCal(function(){
secondFunction(function(){
thirdFunction(function(){
//.....
});
});
});
这种代码在风格上极难阅读,所以有一个专门的名词来形容它,就是回调地狱。它简直是可维护代码的天敌,随时代码越来越多,这种代码非常难以阅读与维护。
这个问题在移动端也存在,虽然移动端并不是单线程的,但它也是以异步回调为主,因为毕竟手机性能有限,不可能允许大量线程来运行一个App。
有问题,就一定会有解决方案。
我并不清楚async/await究竟是在哪里第一个引入,但对于JavaScript这个语言,终于有一天引入了async/await这个特性。
这个特性的作用就是:
让异步回调的代码看起来和同步风格一样,以提升代码的可阅读性
有了async/await之后,代码就变成如下这个样式:
const result = await executeSomeBigCal();
console.log("结果是:" + result);
看到没,是不是和同步的代码一样了?
它改善了回调地狱的问题,便得代码更简洁易懂,又保持了异步的机制下的性能优势,它只是让代码样子上看起来像是同步,但实际并未阻塞任何线程。
当然。它也有问题,就是错误处理方面可能需要特别注意,这是后话,在这就不详细表述了。
异步编程这种模式在性能上的优势,引发了较大的关注,虽然主流的Java或是Spring Boot还是保持着传统的线程阻塞模式,但也出现了响应式编程这种模式,就是基于异步机制的模式。
在性能上,异步编程的优势非常明显。
所以它有日渐流行的趋势。
比如Spring基于异步推出了Spring WebFlux,还有Vert.x也比较有知名度,国内的话比如字节跳动开源的CloudWeGo,核心也是使用Netpoll这个高性能的非阻塞I/O异步实现,不过它用的是GO而非Java。
我的myddd-vertx是基于Kotlin与Vert.x的响应式领域驱动框架,它兼具Kotlin带来的优雅简洁与Vert.x带来的异步高效,让编程简直成为一种享受
一旦使用异步非阻塞这种实现,就一定绕不开代码风格这个问题。
所以,在后端开发,移动端开发以及前端开发,都可以使用异步机制来编程了,它在性能上具有极大的优势。而且几乎每个方向都有async/await这个特性。
用实际的代码来做一些说明吧。
代码摘自我在2020年基于Electron开发的一个跨平台软件
#TypeScript代码
public static async syncFavors(): Promise<Contact[]> {
//从网络获取星标联系人,这实质上是一个异步行为
const favors = await Contact.getNet().fetchFavorContacts();
if (favors) {
//存储到数据库中,这也是一个异步操作
await Contact.getRespository().batchSaveFavors(favors);
}
return favors;
}
代码摘自我的myddd-vertx开源框架,基于Kotlin与Vert.x的响应式领域驱动基础框架
@Test
fun testAddComment(vertx: Vertx, testContext: VertxTestContext){
GlobalScope.launch {
val comment = createComment()
//kotin中的await()
val created = commentRepository.createComment(comment).await()
testContext.verify {
Assertions.assertTrue(created.id > 0)
Assertions.assertEquals(created.id,created.rootCommentId)
Assertions.assertEquals(created.level,0)
}
testContext.completeNow()
}
}
而在后端编码中,Java语言本身没有async/await这种风格,但它也提供了FutureTask这种机制,和async/await相似。而且Java的生态中诸如guava也提供了类似的ListenableFuture的机制。
另外在Java生态中,还有非常流行的RxJava这种流式风格的异步编程。也非常值得关注。
Android也是使用的Kotlin,所以风格与上面这个基本类似。
//在Android中使用async及await,实质是基于Kotlin协程
suspend fun fetchTwoDocs() =
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
所以,可以看到,在后端,移动端,前端你都可以使用这种async/await这种风格。
唯独iOS的Swift,直到5.5这个版本之前,都不支持这个特性。
在Swfit 5.5版本以前,同样可能存在回调地狱的问题。
func makeSandwich(completionBlock: (result: Sandwich) -> Void) {
cutBread { buns in
cutCheese { cheeseSlice in
cutHam { hamSlice in
cutTomato { tomatoSlice in
let sandwich = Sandwich([buns, cheeseSlice, hamSlice, tomatoSlice]
completionBlock(sandwich))
}
}
}
}
}
makeSandwich { sandwich in
eat(sandwich)
}
在支持了async/await特性后,上面的代码可以简化为:
async func cutBread() -> Bread
async func cutCheese() -> Cheese
async func cutHam() -> Ham
async func cutTomato() -> Vegetable
async func makeSandwich() -> Sandwich {
let bread = await cutBread()
let cheese = await cutCheese()
let ham = await cutHam()
let tomato = await cutTomato()
return Sandwich([bread, cheese, ham, tomato])
}
同样功能的代码在简洁性上的差别是不是天差地别?
当然,就这个回调地狱,也不只是async/await一个解决方案,上面我也提供RxJava这种流式编程风格的方式,也是解决方案的一种。
只不过,相对而说,Rx这种模式,对习惯了同步模式下的编程人员来说,仍然存在学习上的曲线问题,而相比较之下,async/await似乎更容易令人接受与学习,它在代码风格上与同步编程风格很类似。
async/await实质并未提供任何新特性,但它极大的简化了异步编程下的代码简洁性与学习曲线。
就算是在后端,Java与Spring Boot仍然占领主流地位的今天,在国内也出现了字节这样的敢于使用GO这样的异步机制的语言,确实令人耳目一新,说明其在技术创新上走在了国内公司的前列,令人佩服。
我认为不可避免的,异步编程会慢慢成为主流,只是对于国内中小企业来说,考虑到人员招聘与技术试错的成本太高,在这种技术创新上仍然会趋向保守,这个转变过程可能不会那么快。
有两个方向的更易于接受,其一是类似字节这样的大企业,有足够招聘到优秀程序员的能力与资本,再一个就是小型的精英团队或个人,出于对自我的信心与技术的追求,愿意采取异步编程的可能性会比较高。
不管你是从事哪一端的开发,我认为,你需要从现在开始,可以关注异步编程了。特别是对于后端开发的程序员,由于习惯了Java与Spring Boot,改变可能存在一定的阻力。但至少你了解一下总归是没错的。
当然,这不代表我否定同步编程这种风格。
技术只是工具,做为程序员,我们需要评估当前的环境,选择最合适的工具。不能因为习惯了什么某种工具,就不管环境什么场景都要使用这种方式。
这其实是一种自我设限。
我们不能被这种自我设限所阻碍。
如果还不太了解异步编程,希望我这篇文章能引起你的兴趣,让你能开始关注下异步编程,因为有了async/await,异步编程并没有想象的那么复杂了。
如果你不知道从何处开始,可以从我的myddd-vertx开源框架开始了解,基于Kotlin与Vert.x的响应式领域驱动基础框架。访问myddd官网(https://myddd.org)以了解更多