认识异步编程

认识异步编程

2023年7月25日发(作者:)

认识异步编程认识异步编程本章主要介绍异步编程的概念和作⽤,Java中异步编程的场景以及不同异步编程场景应使⽤什么技术实现。1.1 异步编程概念与作⽤通常Java开发⼈员喜欢使⽤同步代码编写程序,因为这种请求(request)/响应(response)的⽅式⽐较简单,并且⽐较符合编程⼈员的的思维习惯;这种做法很好,直到系统出现性能瓶颈。在使⽤同步编程⽅式时,由于每个线程同时只能发起⼀个请求并同步等待返回,所以为了提⾼系统性能,此时我们就需要引⼊更多的线程来实现并⾏化处理。但是多线程下对共享资源进⾏访问时,不可避免会引⼊资源争⽤和并发问题;另外,操作系统层⾯对线程的个数是有限制的,不可能通过⽆限制的增加线程来提供系统性能;⽽且,使⽤同步阻塞的编程⽅式还会浪费资源,⽐如发起⽹络IO请求时,调⽤线程就会处于同步阻塞等待响应结果的状态,⽽这时候调⽤线程明明可以去做其他事情,等⽹络IO响应结构返回后再对结构进⾏处理。可见通过增加单机系统线程个数的并⾏编程⽅式并不是“灵丹妙药”。通过编写异步、⾮阻塞的代码,则可以使⽤相同的底层资源将执⾏切换到另⼀个活动任务,然后在异步处理完成后再返回到当前线程继续处理,从⽽提⾼系统性能。异步编程是可以让程序并⾏运⾏的⼀种⼿段,其可以让程序中的⼀个⼯作单元与主应⽤程序线程分开独⽴运⾏,并且在⼯作单元运⾏结束后,会通知主应⽤程序线程它的运⾏结果或者失败原因。使⽤异步编程可以提⾼应⽤程序的性能和响应能⼒等。⽐如当调⽤线程使⽤异步⽅式发起⽹络IO请求后,调⽤线程就不会同步阻塞等待响应结果,⽽是在内存保存请求上下⽂后,马上返回去做其他事情,等⽹络IO响应结果返回后再使⽤IO线程通知业务线程响应结果已经返回,由业务线程对结果进⾏处理。可见,异步调⽤⽅式提⾼了线程的利⽤率,让系统有更多的线程资源来处理更多的请求。⽐如在移动应⽤程序中,在⽤户操作移动设备屏幕发起请求后,如果是同步等待后台服务器返回结果,则当后台服务操作⾮常耗时时,就会造成⽤户看到移动设备屏幕冻结(⼀直处于请求处理中),在结果返回前,⽤户不能操作移动设备的其他功能,这对⽤户体验⾮常不好。⽽使⽤异步编程时,当发起请求后,调⽤线程会发⽣返回,具体返回结果会通过UI线程异步进⾏渲染,且在这期间⽤户可以使⽤移动设备的其他功能。1.2 异步编程场景在⽇常开发中我们经常会遇到这样的情况,即需要异步处理⼀些事情,⽽不需要知道异步任务的结果。⽐如在调⽤线程⾥⾯异步打⽇志,为了不让⽇志打印阻塞调⽤线程,会把⽇志设置为异步⽅式。如图1-1所⽰的⽇志异步化打印,使⽤⼀个内存队列把⽇志打印异步化,然后使⽤单⼀消费线程异步处理内存队列中的⽇志事件,执⾏具体的⽇志落盘操作(本质是⼀个多⽣产单消费模型),在这种情况下,调⽤线程把⽇志任务放⼊队列后会继续执⾏其他操作,⽽不再关⼼⽇志任务具体是什么时候⼊盘的。图 1-1 ⽇志异步打印.png图 1-1 ⽇志异步打印在Java中,每当我们需要执⾏异步任务时,可以直接开启⼀个线程来实现,也可以把异步任务封装为任务对象投递到线程池中来执⾏。在Spring框架中提供了@Async注解把⼀个任务异步化来进⾏处理,具体会在后⾯的章节详细讲解。有时候我们还需要在主线程等待异步任务的执⾏结果,这时候Future就派上⽤场了。⽐如调⽤线程要等任务A执⾏完毕后再顺序执⾏任务B,并且把两者的任务结果拼接起来供前端展⽰使⽤,如果调⽤线程是同步调⽤两次任务(如图 1-2所⽰),则整个过程耗时为执⾏任务A的耗时加上执⾏任务B的耗时。图 1-2 同步调⽤.png图 1-2 同步调⽤如果使⽤异步编程(如图 1-3所⽰),则可以在调⽤线程内开启⼀个异步运⾏单元来执⾏任务A,开启异步运⾏单元后调⽤线程会马上返回⼀个Future对象(futureB),然后调⽤线程本⾝来执⾏任务B,等任务B执⾏完毕后,调⽤线程可以调⽤futureB的get()⽅法获取任务A的执⾏结果,最好再拼接两者的结果。这时由于任务A和任务B是并⾏运⾏的,所以整个过程耗时为max(调⽤线程执⾏任务B的耗时,异步运⾏单元执⾏任务A的耗时)。图 1-3 异步调⽤.png图 1-3 异步调⽤可见整个过程耗时显著缩短,对于⽤户来说,页⾯响应时间缩短,⽤户体验会更好,其中异步单元的执⾏⼀般是由线程池中的线程执⾏。使⽤Future确实可以获取异步任务的执⾏结果,但是获取其结果还是会阻塞调⽤线程的,并没有实现完全的异步化处理,所以在JDK8中提供了CompletableFuture来弥补其缺点。CompletableFuture类允许⾮阻塞⽅式和基于通知的⽅式处理结果,其通过设置回调函数⽅式,让主线程彻底解放出来,实现了实际意义上的异步处理。如图 1-4 所⽰,使⽤CompletableFuture时,当异步单元返回futureB后,调⽤线程可以在其上调⽤whenComplete⽅法设置⼀个回调函数action,然后调⽤线程就会马上返回,等异步任务执⾏完毕后会使⽤异步线程来执⾏回调函数action,⽽⽆须调⽤线程⼲预。如果你对CompletableFuture不了解,没关系,后⾯章节我们会详细讲解,这⾥你只需要知道其解决了传统Future的缺陷就可以了。图 1-4 CompletableFuture异步执⾏.png图 1-4 CompletableFuture异步执⾏JDK8还引⼊了Stream,旨在有效地处理数据流(包括原始类型),其使⽤声明式编程让我们可以写出可读性、可维护性很强的代码,并且结合CompletableFuture完美地实现异步编程。但是它产⽣的流只能使⽤⼀次,并且缺少与实际相关的操作(例如RxJava中基于时间窗⼝的缓存元素),虽然可以执⾏并⾏计算,但⽆法指定要使⽤的线程池。同时,它也没有设计⽤于处理延迟的操作(例如RxJava中的defer操作),所以Reactor、RxJava等Reactive API就是为了解决这些问题⽽⽣的。Reactor、RxJava等反应式API也提供Java 8 Stream 的运算符,但它们更适⽤于流序列(不仅仅是集合),并且允许定义⼀个转换操作的管道,该管道将应⽤于通过它的数据(这要归功于⽅便的流畅API和Lambda表达式的使⽤)。Reactive旨在处理同步或异步操作,并允许你对元素进⾏缓存(buffer)、合并(merge)、连接(join)等各种转换。上⾯我们讲解了但JVM内的异步编程,那么对于跨⽹络的交互是否也存在异步编程范畴呢?对于⽹络请求来说,同步调⽤是⽐较直截了当的。⽐如我们在⼀个线程A中通过RPC请求获取服务B和服务C的数据,然后基于两者的结果做⼀些事情。在同步调⽤情况下,线程A需要调⽤服务B,然后同步等待服务B结果返回后,才可以对服务C发起调⽤,等服务C结果返回后才可以结合服务B和C的结果执⾏其他操作。如图 1-5所⽰,线程A同步获取服务B的结果后,再同步调⽤服务C获取结果,可见在同步调⽤情况下业务执⾏语义⽐较清晰,线程A顺序地对多个服务请求进⾏调⽤;但是同步调⽤意味着当前发起请求的调⽤线程在远端机器返回结果前必须阻塞等待,这明细很浪费资源。好的做法应该是在发起请求的调⽤线程发起请求后,注册⼀个回调函数,然后马上返回去执⾏其他操作,当远端把结果返回后再使⽤IO线程或者框架线程池中的线程执⾏回调函数。图 1-5 同步RPC调⽤.png图 1-5 同步RPC调⽤那么如何实现异步调⽤?在Java中NIO的出现让实现上⾯的功能变得简单,⽽⾼性能异步、基于事件驱动的⽹络编程框架Netty的出现让我们从编写繁杂的Java NIO程序中解放出来,现在的RPC框架,⽐如Dubbo底层⽹络通信,就是基于Netty实现的。Netty框架将⽹络编程逻辑与业务逻辑处理分离开来,在内部帮我们⾃动处理好⽹络与异步处理逻辑,让我们专⼼写⾃⼰的业务处理逻辑,⽽Netty的异步⾮阻塞能⼒与CompletableFuture结合则可以轻松地实现⽹络请求的异步调⽤。在执⾏RPC(远程过程调⽤)调⽤时,使⽤异步编程可以提⾼系统的性能。如图 1-6所⽰,在异步调⽤情况下,当线程A调⽤服务B后,会马上返回⼀个异步的futureB对象,然后线程A可以在futureB上设置⼀个回调函数;接着线程A可以继续访问服务C,也会马上返回⼀个futureC对象,然后线程A可以在futureC上设置⼀个回调函数。图 1-6 RPC异步调⽤.png图 1-6 RPC异步调⽤如图 1-6 可知,在异步调⽤情况下,线程A可以并发地调⽤服务B和服务C,⽽不再是顺序的。由于服务B和服务C是并发运⾏,所以相⽐同步调⽤,线程A获取到服务B和服务C结果的时间会缩短很多(同步调⽤情况下的耗时为服务B和服务C返回结果耗时的,异步调⽤情况下函数为max(服务B耗时,服务C耗时))。另外,这⾥可以借助CompletableFuture的能⼒等两次RPC调⽤都异步返回结果后再执⾏其他操作,这时候调⽤流程如图 1-7所⽰。图 1-7 合并RPC调⽤结果.png图 1-7 合并RPC调⽤结果如图 1-7 所⽰,调⽤线程A⾸先发起服务B的远程调⽤,会马上返回⼀个futureB对象,然后发起服务C的远程调⽤,也会马上返回⼀个futureC对象,最好调⽤线程A使⽤代码mbine(futureC,action)等futureB和futureC结果可⽤时执⾏回调函数action。这⾥我们只是简单概述下基于Netty的异步⾮阻塞能⼒以及CompletableFuture的可编排能⼒,基于这些能⼒,我们可以实现功能很强⼤的异步编程能⼒。在后⾯章节,我们会以Dubbo框架为例讲解其借助Netty的⾮阻塞异步API实现服务消费端的异步调⽤。其实,有了CompletableFuture实现异步编程,我们可以很⾃然地使⽤适配器来实现Reactive风格的编程。当我们使⽤RxJava API时,只需要使⽤Flowable的⼀些函数转转CompletableFuture为Flowable对象即可,这个我们在后⾯章节也会讲述。上节讲解了⽹络请求中RPC框架的异步请求,其实还有⼀类,也就是Web请求,在Web应⽤中Servlet占有⼀席之地。在Servlet3.0规范前,Servlet容器对Servlet的处理都是每个请求对应⼀个线程这种1:1的模式进⾏处理的(如果 1-8 所⽰),每当收到⼀个请求,都会开启⼀个Servlet容器内的线程来进⾏处理,如果Servlet内处理⽐较耗时,则会把Servlet容器内线程使⽤耗尽,然后容器就不能再处理新的请求了。图 1-8 Servlet的阻塞处理模型.png图 1-8 Servlet的阻塞处理模型Servlet 3.0 规范中则提供了异步处理的能⼒,让Servlet容器中的线程可以及时释放,具体Servlet业务处理逻辑是在业务⾃⼰的线程池内来处理;虽然Servlet 3.0 规范让Servlet的执⾏变为异步,但是其IO还是阻塞式的。IO阻塞是说在Servlet处理请求时,从ServletInputStream中读取请求体时是阻塞的,⽽我们想要的是当书记就绪时直接通知我们去读取就可以了,因为这可以避免占⽤我们⾃⼰的线程来进⾏阻塞读取,好在Servlet 3.1 规范提供了⾮阻塞IO来解决这个问题。虽然Servlet技术栈的不断发展实现了异步处理与⾮阻塞IO,但是其异步是不彻底的,因为受制于Servlet规范本⾝,⽐如其规范是同步的(Filter,Servlet)或阻塞的(getParameter,getPart)。所以新的使⽤少量线程和较少的硬件资源来处理并发的⾮阻塞Web技术栈应运⽽⽣-WebFlux,其是与Servlet技术栈并⾏存在的⼀种新技术,基于JDK8函数式编程与Netty实现天然的异步、⾮阻塞处理,这些我们在后⾯的章节会具体介绍。为了更好的实现异步编程,降低异步编程的成本,⼀些框架也应运⽽⽣,⽐如⾼性能线程间消息传递库Disruptor,其通过为事件(event)预先分配内存、⽆锁CAS算法、缓存⾏填充、两阶段协议提交来实现多线程并发地处理不同的元素,从⽽实现⾼性能的异步处理。⽐如Akka基于Actor模式实现了天然⽀持分布式的使⽤消息进⾏异步处理的服务;⽐如⾼性能分布式消息中间件Apache RocketMetaQ实现了应⽤间的异步解耦、流量肖锋。⼀些新兴的语⾔对异步处理的⽀持能⼒让我们忍不住称赞,Go语⾔就是其中之⼀,其通过语⾔层⾯内置的goroutine与channel可以轻松实现复杂的异步处理能⼒。以上就是本书要讨论的内容。1.3 总结本章我们⾸先概要介绍了异步编程的概念与作⽤,让⼤家对异步编程有⼀个⼤致的了解;然后讲解了Java中异步编程的场景,让⼤家通过实际场景案例进⼀步了解异步编程是什么,以及不同异步编程场景应使⽤什么技术来实现。

发布者:admin,转转请注明出处:http://www.yc00.com/news/1690218862a316772.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信