本文转载自微信公众号《编译了一个程序》,作者Yasinx。转载本文请联系编辑程序公众号。最近在学习协程,打算输出几篇介绍协程的文章。协程与异步密切相关,所以我想先介绍一下异步。异步是一种运行程序的方式,各种编程语言都或多或少地支持它。异步对于Java后端程序员来说并不是一个特别熟悉的概念,Android或者前端同学可能对异步这个概念比较熟悉。程序同步和异步同步是最简单最适合我们人类思维的编程方式。所谓同步,就是程序会逐行执行,执行完一个语句后执行下一句。同步代码是这样的:stepA();stepB();stepC();...stepA执行完后,stepB执行完,stepB执行完后,stepC执行完。而有时候我们会有这样的需求:在后台执行一个程序。具体在我们的例子中,stepA执行完之后,还需要执行stepB,但是stepC可以马上执行,不需要等stepB执行完。于是异步编程就出来了。在Java语言中,我们可以新建一个线程(或者使用线程池)来执行异步任务:stepA();newThread(()->stepB()).start();stepC();它在另一个线程中“异步”执行,但stepC继续在当前线程中执行。异步有什么好处?有一个明显的好处:使程序“响应”。比如上面的例子,如果stepB()这个任务比较耗时,比如发邮件。然后使用同步方式,程序需要等待这里的卡完成stepB再往下走。而如果采用异步方式,stepB可以在“后台”执行,不会影响当前程序的执行。这一点在UI程序中尤为重要,毕竟界面的响应时间对用户体验来说非常重要。所以与UI相关的语言和框架是最先研究和尝试异步技术的。比如RxJava起源于Android,Kotlin、Dart、JavaScript等语言也广泛应用于UI程序。同样,对于IO密集型程序,使用异步也能显着提升性能。大家熟悉的nginx、redis、netty等底层都是使用操作系统的系统调用(比如linux的epoll)来实现异步。实现高性能。使用异步在Java中使用异步一般是用多线程实现的。正如我们上面提到的,我们可以启动一个新线程来“后台”异步任务。当然,我们也可以把它扔到线程池中。//创建一个新的线程来执行异步任务newThread(()->stepB()).start();但是如果我们想使用异步返回结果呢?例如,一个常见的场景是请求另一个微服务接口。JDK1.5提供了Callable和Future接口来实现带有“返回值”的多线程任务。使用的时候一般是配合线程池使用:publicstaticvoidmain(String[]args)throwsException{ExecutorServiceexecutor=Executors.newSingleThreadExecutor();Futurefuture=executor.submit(()->{//模拟IO取一秒钟Thread.sleep(1000);return"hello";});System.out.println("submitted");//这会阻塞直到future.get返回一个值或者超时System.out.println(future.get(2,TimeUnit.SECONDS));executor.shutdown();如果使用Future,当我们调用future.get()方法时,会阻塞直到异步任务返回结果或者抛出异常或者超时。想象一下我们有这样一个需求:任务B1需要任务B的结果,任务C1需要任务C的结果,但是他们是相互独立的。如果我们使用Future,我们必须这样做:));//这一步必须等待stepB1执行完stepC1(futureC.get());所以使用future实际上会在调用get方法时阻塞主进程。那么有什么办法不阻塞呢?解决方案是使用回调。回调和回调地狱所谓回调,在函数式编程语言里,就是我传入一个函数,在异步任务完成后执行这个函数。虽然Java不是函数式编程语言,但是Java8也支持函数式编程。假设我们的需求只是打印出一个异步任务产生的结果字符串,我们可以这样写:publicstaticvoidmain(String[]args)throwsException{Consumercallback=System.out::println;newThread(()->{//模拟api调用,省略try-catchThread.sleep(1000);//假设这是调用第三方api返回的字符串Strings="hello";callback.accept(s);}).start();System.out.println("started");}你甚至可以把程序代码段直接放到异步任务中,不用回调函数:publicstaticvoidmain(String[]args)throwsException{newThread(()->{//模拟api调用,省略try-catchThread.sleep(1000);//假设这是调用第三方api返回的字符串Strings="hello";print(s);}).start();System.out.println("started");}privatestaticvoidprint(Stringstr){System.out.println(str);}如果异步任务需要回调太多怎么办?比如我们需要先异步请求接口A,得到结果B后去异步请求接口,得到结果C后去异步请求接口:publicstaticvoidmain(String[]args)throwsException{newThread(()->{StringresultA=callAPI("input","a");newThread(()->{StringresultB=callAPI(resultA,"b");newThread(()->{StringresultC=callAPI(resultB,"c");System.out.println(结果C);}).start();}).start();}).start();System.out.println("started");}privatestaticStringcallAPI(Stringparam,StringmockRes){//模拟api调用,省略try-catchThread.sleep(1000);returnmockRes;}是不是觉得这层嵌套的代码很丑?这就是臭名昭著的“回调地狱”。Java8提供了一个名为CompletableFuture的类来支持一些异步功能,包括回调。支持“链式调用”,一定程度上可以解决“回调地狱”的问题。上面的代码可以使用CompletableFuture编写,如下所示:"b")).thenApply(res->callAPI(res,"c")).thenAccept(System.out::println);System.out.println("started");//等待异步任务输出Thread.sleep(20000);}响应式编程是另一种异步解决方案。它的主要应用场景是异步处理数据集合。目标是同步的Iterable。下面是一张对比图:一个典型的场景就是UI产生的事件流(比如点击事件等)。反应式编程的核心是“观察者模式”。客户端发送一个请求,可以立即得到一个Stream返回,客户端订阅这个Stream来接收通知。当服务端有数据时,会将数据发布到Stream中,客户端就可以接收数据了。Spring5还支持响应式编程,相信这将是未来web编程的一大趋势。反应流APIjava.util.concurrent.flow已正式成为Java9的一部分。但是现在发展还是比较慢,大家对这个东西的接受度一般,可能是因为切换成本比较高,目前webmvc可以满足大部分需求。协程看了一圈资料,很多文章都在讨论什么是协程。我初步总结出协程主要有两个作用:可以用同步的方式写异步代码,可以在适当的时候挂起当前程序片段,在适当的时候恢复。这是一个可以由代码控制并由程序控制的协程。同一个线程在内部工作。在大多数IO成为瓶颈的应用场景下,可以替代目前主流的多线程模型,节省线程切换的开销,提高吞吐量。以后有时间再详细介绍协程。作者简介我是Yasin,一个爱写博客的技术人个人网站:https://yasinshaw.com