告别异步代码

Quasar是一个向JVM添加真正的轻量级线程(纤维)的库。 它们非常便宜且非常快-实际上,光纤的行为就像Erlang进程或Go goroutines-并允许您编写简单的阻塞代码,同时享受与复杂异步代码相同的性能优势。

在本文中,我们将学习如何将任何基于回调的异步API转换为漂亮的(光纤)阻塞API。 它适用于希望将自己的或第三方库与Quasar光纤集成的用户。 如果您仅将Quasar光纤与通道或演员一起使用,或者利用Comsat项目中已经提供的许多集成功能,则不需要了解这些知识(下面提供的代码是应用程序开发人员从未看到的代码)。 但是,即使您不这样做,您也可能会发现这篇文章对理解Quasar如何发挥其魔力很有帮助。

为什么要异步?

首先,许多库提供异步API的原因是OS可以处理的正在运行的1个线程的数量远远少于OS可以维护的开放TCP连接的数量。 也就是说,您的计算机所支持的并发性比线程所提供的要高得多,因此,库以及使用它们的开发人员会放弃线程,将其作为用于软件并发性单元2的抽象。 异步API不会阻塞线程,并可能导致显着的性能提升(通常在吞吐量和服务器容量方面,而在延迟方面却没有那么多)。

但是,使用异步API也会创建正确获得“回调地狱”名称的代码。 在缺乏多核处理的环境(例如Javascript)中,回调地狱已经很糟糕了。 在那些需要关注内存可见性和同步性的环境(例如JVM)中,情况可能会更糟。

编写在光纤上运行的阻塞代码可为您提供与异步代码相同的优点,而没有缺点:您使用了不错的阻塞API(甚至可以继续使用现有的API),但是却获得了非阻塞代码的所有性能优势。

可以肯定的是,异步API还有一个优势:它们使您可以同时分派多个IO操作(例如HTTP请求)。 因为这些操作通常需要很长时间才能完成,而且通常是独立的,所以我们可以同时等待其中的几个完成。 但是,Java期货也可以使用此有用的功能,而无需回调。 稍后,我们将看到如何制作纤维增强期货。

光纤异步

许多现代的Java IO /数据库库/驱动程序都提供两种API:一种是同步(线程)阻塞的API,另一种是基于回调的异步API(对于NIO,JAX-RS客户端,Apache HTTP客户端以及更多的API来说都是如此。 )。 同步API更好。

Quasar有一个编程工具,可以将任何基于回调的异步API转换为一个很好的阻止光纤的API: FiberAsync 。 本质上, FiberASync所做的是阻止当前光纤,安装异步回调,并在触发该回调时,它将再次唤醒光纤,并返回操作结果(如果失败,则引发异常)。

为了了解如何使用FiberAsync ,我们将看一个API示例: FooClientFooClient是一种现代的IO API,因此有两种形式,一种是同步的,线程阻止一种,另一种是异步的。 他们来了:

interface FooClient {String op(String arg) throws FooException, InterruptedException;
}interface AsyncFooClient {Future<String> asyncOp(String arg, FooCompletion<String> callback);
}interface FooCompletion<T> {void success(T result);void failure(FooException exception);
}

请注意异步操作(如许多现代库中的情况)如何都需要回调并返回前途。 现在,让我们忽略未来。 我们稍后再讲。

FooClientAsyncFooClient更好,更简单,但是它阻塞了线程并大大降低了吞吐量。 我们想要创建一个FooClient接口的实现,该接口可以在光纤中运行并阻塞光纤,因此我们获得了简单的代码高吞吐量。 为此,我们将在AsyncFooClient使用AsyncFooClient ,但将其转换为阻止光纤的FooClient 。 这是我们需要的所有代码(我们将进一步对其进行简化):

public class FiberFooClient implements FooClient {private final AsyncFooClient asyncClient;public FiberFooClient(AsyncFooClient asyncClient) {this.asyncClient = asyncClient;}@Override@SuspendableString op(final String arg) throws FooException, InterruptedException {try {return new FiberAsync<String, FooException>() {@Overrideprotected void requestAsync() {asyncClient.asyncOp(arg, new FooCompletion<String>() {public void success(String result) {FiberAsync.this.asyncCompleted(result);}public void failure(FooException exception) {FiberAsync.this.asyncFailed(exception);}});}}.run();} catch(SuspendExecution e) {throw new AssertionError(e);}}
}

现在,这是怎么回事? 我们正在实施的FooClient接口,但我们正在做op纤维粘连,而不是线程阻塞。 我们需要告诉Quasar我们的方法是光纤阻塞(或“可挂起”),因此我们使用@Suspendable对其进行@Suspendable

然后,我们将FiberAsync子类FiberAsync并实现requestAsync方法( FiberAsync接受的两个通用类型参数是返回类型和操作可能抛出的已检查异常的类型(如果有);对于没有已检查异常的情况,第二个通用参数应为RuntimeException )。 requestAsync负责启动异步操作并注册回调。 然后,回调需要调用asyncCompleted (如果操作成功)并将其传递给我们希望返回的结果,或者asyncFailed (如果操作失败)并将失败原因的异常传递给它。

最后,我们调用FiberAsync.run() 。 这将阻止当前的光纤,并调用requestAsync来安装回调。 纤维将保持阻塞,直到回调被触发,它会释放出FiberAsync通过调用或者asyncCompletedasyncFailedrun方法还具有一个带超时参数的版本,如果我们想对阻塞操作进行时间限制(通常是个好主意),该方法很有用。

需要解释的另一件事是try/catch块。 有两种方法来声明可@Suspendable的方法:用@Suspendable对其进行注释,或者声明它以引发已检查的异常SuspendExecutionFiberAsyncrun方法使用了后者,因此为了编译代码,我们需要捕获SuspendExecution ,但是由于这不是真正的异常,因此我们永远无法真正捕获它(嗯,至少在Quasar运行正常的情况下,至少不是这样) –因此为AssertionError

完成后,您可以在任何光纤中使用op ,如下所示:

new Fiber<Void>(() ->{// ...String res = client.op();// ...
}).start();

顺便说一句,所有的要短很多与脉冲星 (类星体的Clojure的API),其中异步操作:

(async-op arg #(println "result:" %))

使用Pulsar的await宏将其转换为以下同步的光纤阻塞代码:

(println "result:" (await (async-op arg)))

简化和批量生产

通常,像FooClient这样的接口将具有许多方法,并且通常, AsyncFooClient大多数方法将采用相同类型的回调( FooCompletion )。 如果是这种情况,我们可以将我们已经看到的许多代码封装到FiberAsync的命名子类中:

abstract class FooAsync<T> extends FiberAsync<T, FooException> implements FooCompletion<T> {@Overridepublic void success(T result) {asyncCompleted(result);}@Overridepublic void failure(FooException exception) {asyncFailed(exception);}@Override@Suspendablepublic T run() throws FooException, InterruptedException {try {return super.run();} catch (SuspendExecution e) {throw new AssertionError();}}@Override@Suspendablepublic T run(long timeout, TimeUnit unit) throws FooException, InterruptedException, TimeoutException {try {return super.run(timeout, unit);} catch (SuspendExecution e) {throw new AssertionError();}}
}

请注意,我们如何使FiberAsync直接实现FooCompletion回调–不是必需的,但这是一个有用的模式。 现在,我们的光纤阻塞op方法要简单得多,并且该接口中的其他操作也可以轻松实现:

@Override
@Suspendable
public String op(final String arg) throws FooException, InterruptedException {return new FooAsync<String>() {protected void requestAsync() {asyncClient.asyncOp(arg, this);}}.run();
}

有时,我们可能希望在常规线程而不是光纤上调用op方法。 默认情况下,如果在线程上调用FiberAsync.run()会引发异常。 为了解决这个问题,我们要做的就是实现另一个FiberAsync方法requestSync ,如果在光纤上调用run ,它将调用原始的同步API。 我们的最终代码如下所示(我们假设FiberFooClass具有类型为FooClientsyncClient字段):

@Override
@Suspendable
public String op(final String arg) throws FooException, InterruptedException {return new FooAsync<String>() {protected void requestAsync() {asyncClient.asyncOp(arg, this);}public String requestSync() {return syncClient.op(arg);}}.run();
}

就是这样!

期货

期货是一种方便的方法,它允许我们在等待所有独立的长时间IO操作完成时同时开始它们。 我们希望我们的纤维能够阻挡期货。 许多Java库通过其异步操作返回期货,因此用户可以在完全异步,基于回调的用法和采用期货的“半同步”用法之间进行选择。 我们的AsyncFooClient接口就是这样工作的。

这是我们实现AsyncFooClient版本的AsyncFooClient ,该版本返回阻止光纤的期货:

import co.paralleluniverse.strands.SettableFuture;public class FiberFooAsyncClient implements FooClient {private final AsyncFooClient asyncClient;public FiberFooClient(AsyncFooClient asyncClient) {this.asyncClient = asyncClient;}@Overridepublic Future<String> asyncOp(String arg, FooCompletion<String> callback) {final SettableFuture<T> future = new SettableFuture<>();asyncClient.asyncOp(arg, callbackFuture(future, callback))return future;}private static <T> FooCompletion<T> callbackFuture(final SettableFuture<T> future, final FooCompletion<T> callback) {return new FooCompletion<T>() {@Overridepublic void success(T result) {future.set(result);callback.completed(result);}@Overridepublic void failure(Exception ex) {future.setException(ex);callback.failed(ex);}@Overridepublic void cancelled() {future.cancel(true);callback.cancelled();}};}
}

如果返回, co.paralleluniverse.strands.SettableFuture返回co.paralleluniverse.strands.SettableFuture ,如果我们在光纤或普通线程(即任何类型的绞线上 )上对其进行阻塞,则同样可以很好地工作。

JDK 8的CompletableFuture和Guava的ListenableFuture

返回到CompletionStage (或实现它的CompletableFuture )的API(在JDK 8中添加到Java中)可以通过预先构建的FiberAsync更加轻松地进行光纤阻塞。 例如,

CompletableFuture<String> asyncOp(String arg);

通过以下方式变成光纤阻塞呼叫:

String res = AsyncCompletionStage.get(asyncOp(arg));

返回Google Guava的方法类似地转换为光纤阻塞同步,因此:

ListenableFuture<String> asyncOp(String arg);

通过以下方式变成光纤阻塞:

String res = AsyncListenableFuture.get(asyncOp(arg));

期货的替代品

尽管期货有用且熟悉,但我们实际上并不需要使用纤维时返回它们的特殊API。 产生的纤维是如此便宜( Fiber类实现了Future ,因此纤维本身可以代替“手工”的期货。 这是一个例子:

void work() {Fiber<String> f1 = new Fiber<>(() -> fiberFooClient.op("first operation"));Fiber<String> f2 = new Fiber<>(() -> fiberFooClient.op("second operation"));String res1 = f1.get();String res2 = f2.get();
}

因此,即使我们使用的API不提供,光纤也可以为我们提供期货。

如果没有异步API怎么办?

有时我们很不幸地遇到一个仅提供同步线程阻塞API的库。 JDBC是此类API的主要示例。 虽然Quasar不能提高使用此类库的吞吐量,但是使API光纤兼容仍然值得(实际上非​​常容易)。 为什么? 因为调用同步服务的光纤也可能做其他事情。 实际上,它们可能很少调用该服务(仅当发生高速缓存未命中时,才考虑从RDBMS读取数据的光纤)。

实现此目的的方法是通过在专用线程池中执行实际的调用,然后通过FiberAsync封装该假的异步API,将阻塞API转变为异步API。 这个过程是如此机械, FiberAsync有一些静态方法可以为我们处理所有事情。 因此,假设我们的服务仅公开了阻塞的FooClient API。 要使其成为光纤阻塞,我们要做的是:

public class SadFiberFooClient implements FooClient {private final FooClient client;private static final ExecutorService FOO_EXECUTOR = Executors.newCachedThreadPool();public FiberFooClient(FooClient client) {this.client = client;}@Override@SuspendableString op(final String arg) throws FooException, InterruptedException {try {return FiberAsync.runBlocking(FOO_EXECUTOR, () -> client.op());} catch(SuspendExecution e) {throw new AssertionError(e);}}
}

FooClient此实现可以安全地用于线程和光纤。 实际上,当在普通线程上调用该方法时,该方法将不会费心将操作分派到所提供的线程池,而是在当前线程上执行该操作–就像我们使用原始FooClient实现那样。

结论

此处显示的技术FiberAsynccpstrands.SettableFuture正是构成Comsat项目的集成模块的工作方式。 Comsat包括Servlet,JAX-RS(服务器和客户端),JDBC,JDBI,jOOQ,MongoDB,Retrofit和Dropwizard的集成。

重要的是要了解如何-创建简单且高性能的光纤阻塞API-我们确实重新实现了API 接口 ,但没有实现其内部工作:仍然仅通过其异步API使用原始库代码,其丑陋之处在于现在对图书馆用户隐藏了。

额外信用:单子怎么样?

除了纤程外,还有其他方法可以处理回调地狱。 JVM世界中最著名的机制是Scala的可组合期货,RxJava的可观察对象以及JDK 8的CompletionStage / CompletableFuture 。 这些都是单子和单子组成的例子。 Monad可以工作,有些人喜欢使用它们,但是我认为对于大多数编程语言来说,它们是错误的方法。

您会看到,单子是从基于lambda演算的编程语言中借用的。 Lambda演算是一种理论计算模型,与Turing机器完全不同,但完全类似。 但是与图灵机模型不同,lambda微积分计算没有步骤,动作或状态的概念。 这些计算没有任何事情; 他们只是。 那么,Monads是Haskell等基于LC的语言将动作,状态,时间等描述为纯计算的一种方式。 它们是LC语言告诉计算机“先执行然后再执行”的一种方法。

问题是,命令式语言已经有了“先做然后再做”的抽象,而这种抽象就是线程。 不仅如此,而且是必须的语言通常有一个非常简单的符号“这样做,然后做”:声明后跟语句。 命令式语言甚至考虑采用这种外来概念的唯一原因是因为(通过OS内核)线程的实现不令人满意。 但是,与其采用一个陌生,陌生的概念(一个需要完全不同类型的API的概念),不如采用一个相似但细微不同的抽象,最好是修复(线程)的实现。 光纤保留抽象并修复实现。

Java和Scala等语言中的monad的另一个问题是,这些语言不仅势在必行,而且还允许不受限制的共享状态突变和副作用-Haskell却没有。 无限制的共享状态突变和“线程”单核的结合可能是灾难性的。 在纯FP语言中-由于副作用是受控的-计算单位(即功能)也是并发单位:您可以安全地同时执行任何一对功能。 当您不受限制的副作用时,情况并非如此。 函数执行的顺序,是否可以同时执行两个函数以及一个函数是否以及何时可以观察到另一个函数执行的共享状态突变都是非常重要的问题。 结果,作为“线程” monad的一部分运行的函数要么必须是纯函数(没有任何副作用),要么必须非常小心如何执行这些副作用。 这正是我们要避免的事情。 因此,尽管单子组合确实比回调地狱生成了更好的代码,但它们不能解决异步代码引入的任何并发问题。

聚苯乙烯

上一节不应理解为像Haskell这样的纯“ FP”语言的认可,因为我实际上认为它们带来了太多其他问题。 我相信(不久的将来)是命令式语言3 ,它将允许共享状态变异但具有一些事务语义。 我相信那些未来的语言将主要从Clojure和Erlang等语言中获得灵感。

  1. 通过运行我的意思是线程往往不够可运行↩
  2. 见利特尔法则,可扩展性和容错 ↩
  3. 它们是否“功能化”是一个难题,因为没有人对功能性编程语言是什么以及它与非功能性语言的区别提出了很好的定义。 ↩

翻译自: https://www.javacodegeeks.com/2015/04/farewell-to-asynchronous-code.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/360278.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何利用多核CPU来加速你的Linux命令

如何利用多核CPU来加速你的Linux命令 原文出处&#xff1a; rankfocus 译文出处&#xff1a; 外刊IT评论 你是否曾经有过要计算一个非常大的数据(几百GB)的需求&#xff1f;或在里面搜索&#xff0c;或其它操作——一些无法并行的操作。数据专家们&#xff0c;我是在对你们说…

魔术二传手反模式

设置者和获取者是邪恶的。 创建JavaBean定义时&#xff0c;这似乎是个好主意。 但是它们对Java社区造成了很大的伤害。 通常不如null指针那么多&#xff0c;但足够了。 首先&#xff0c;许多初级人员认为实现setter和getter&#xff08;嘿&#xff0c;在Eclispe中只需单击几下…

sqlwarning mysql_mysql提示[Warning] Invalid (old?) table or database name问题的解决方法

DROP TABLE IF EXISTS [TEMP_TABLE_NAME];create temporary table [TEMP_TABLE_NAME] select col1,col2,... from [TABLE_NAME];alter table [TEMP_TABLE_NAME] add unique idx_col1(col1);经过以上操作中&#xff0c;多次出现该warning问题。通过查询和跟踪调试源码&#xff0…

C语言操作符优先级

转自&#xff1a;http://www.cnblogs.com/xiehy/archive/2010/02/04/1663825.html 优先级 运算符 含 义 要求运算 对象的个数 结合方向 1 () [] -> . 圆括号 下标运算符 指向结构体成员运算符 结构体成员运算符 自左至右 2 ! 逻辑非运算符 1 (单目运算符)…

linux mysql select_MySQL-Select语句高级应用

阅读目录1.1 SELECT高级应用1.2 select中where子句使用1.3 select中ORDER BY子句1.4 LIMIT子句1.5 多表连接查询1.6 Informatica_schema获取元数据1.7 参考文献1.1 SELECT高级应用1.1.1 前期准备工作本次测试使用的是world数据库&#xff0c;由MySQL官方提供下载地址&#xff1…

为AWT的机器人创建DSL

Java SDK附带了java.awt.Robot类&#xff0c;该类允许键盘和鼠标输入的自动化以及屏幕捕获的创建。 如果您想编写一个模拟用户输入的小型测试应用程序&#xff0c;或者只想自动化一些重复文本的输入&#xff0c;则此功能非常有用。 但是您不想每次都编写一个完整的Java应用程序…

Win7下硬盘安装Redhat双系统

Win7下硬盘安装Redhat Linux 形成双系统过程详解 需要软件 EasyBCD2.0 和 linux ISO 系统镜像 RedHat linux下载地址&#xff1a;http://www.linuxidc.com/Linux/2013-01/78017.htm 安装前准备工作: 1 一个 Windows 盘 D E F 任选其一都可以&#xff0c;将其格式化为FAT32 格式…

java rmi漏洞工具_学生会私房菜【20200924】Weblogic WLS核心组件反序列化命令执行突破(CVE20182628)漏洞复现...

学生会私房菜学生会私房菜是通过学生会信箱收集同学们的来稿&#xff0c;挑选其中的优质文档&#xff0c;不定期进行文档推送的主题。本期文档内容为&#xff1a;Weblogic WLS核心组件反序列化命令执行突破(CVE-2018-2628)漏洞复现》作者介绍&#xff1a;ChowChow&#xff0c;一…

Java泛型中的多态

从作为Java程序员的早期开始&#xff0c;我们都知道如何实例化和使用Collection对象。 实例化为具体类的List接口将如下所示。 List myArrayList new ArrayList();如果myArrayList应该仅保存Integer对象&#xff0c;则从Java 5编译器开始&#xff0c;按照Java Generics规范…

ASP.NET伪静态-无法读取配置文件,因为它超过了最大文件大小的解决办法

一直都在使用微软URLRewriter&#xff0c;具体的使用方法我就不多说了&#xff0c;网上文章很多。 但最近遇到一个问题&#xff0c;就是当web.config文件里面设置伪静态规则过多&#xff0c;大于2M的时候&#xff0c;就报错&#xff1a;无法读取配置文件&#xff0c;因为它超过…

java定义list_我的Java Web之路59 - Java中的泛型

本系列文章旨在记录和总结自己在Java Web开发之路上的知识点、经验、问题和思考&#xff0c;希望能帮助更多(Java)码农和想成为(Java)码农的人。目录介绍再谈Java中的类型为什么需要泛型&#xff1f;Java中的泛型泛型类型泛型方法总结介绍还记得我在这篇文章(我的Java Web之路3…

Spring查找方法示例

当一个bean依赖于另一个bean时&#xff0c;我们使用setter属性或通过构造函数注入bean。 getter方法将向我们返回已设置的引用&#xff0c;但是假设您每次调用getter方法时都想要一个依赖bean的新实例&#xff0c;那么您可能将不得不采用另一种方法。 在本文中&#xff0c;我…

mysql判断数字的函数_Mysql必读MySql判断汉字、日期、数字的具体函数

《Mysql必读MySql判断汉字、日期、数字的具体函数》要点&#xff1a;本文介绍了Mysql必读MySql判断汉字、日期、数字的具体函数&#xff0c;希望对您有用。如果有疑问&#xff0c;可以联系我们。MYSQL学习几个平常用的mysql函数,MySql判断汉字、日期、数字的具体函数分享给大家…

编码:可视化位图

在过去的一个月左右的时间里&#xff0c;我每天花费一些时间来阅读Neo4j代码库的新部分&#xff0c;以使其更加熟悉&#xff0c;而我最喜欢的类之一是Bits类&#xff0c;该类可以完成所有底层工作&#xff0c;并且到磁盘。 特别是&#xff0c;我喜欢它的toString方法&#xff…

通过更改透明度使图片为透明

使用AlphaBlend函数 函数功能 该函数用来显示具有指定透明度的图像。函数原型 AlphaBlend(HDC hdcDest,int nXOriginDest,int nYOriginDest,int nWidthDest,int hHeightDest,HDC hdcSrc,int nXOriginSrc,int nYOriginSrc,int nWidthSrc,int nHeightSrc,BLENDFUNCTION blendFunc…

(转)CocoaPods:管理Objective-c 程序中各种第三方开源库关联

在我们的iOS程序中&#xff0c;经常会用到多个第三方的开源库&#xff0c;通常做法是去下载最新版本的开源库&#xff0c;然后拖拽到工程中。 但是&#xff0c;第三方开源库的数量一旦比较多&#xff0c;版本的管理就非常的麻烦。有没有什么办法可以简化对第三方库的管理呢&…

为什么子进程每次执行顺序不一样_看完这篇还不懂Redis的RDB持久化,你来打我...

推荐观看&#xff1a;Redis缓存穿透的终极解决方案&#xff0c;手写布隆过滤器_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili​www.bilibili.comP8架构师串讲&#xff1a;Redis&#xff0c;zookeeper&#xff0c;kafka&#xff0c;Nginx等技术_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili​w…

Spring XD用于数据提取

Spring XD是一个功能强大的工具&#xff0c;它是一组可安装的Spring Boot服务&#xff0c;可以独立运行&#xff0c;在YARN或EC2之上运行。 Spring XD还包括一个管理UI网站和一个用于作业和流管理的命令行工具。 Spring XD是一组功能强大的服务&#xff0c;可与各种数据源一起使…

JDK 9 REPL:入门

会议是聚会Java名人的好地方。 Devoxx France是与Java语言架构师&#xff0c;前同事和老朋友Brian Goetz&#xff08; briangoetz &#xff09;见面的一个机会。 我们谈论了JDK 9&#xff0c;而他全都热衷于REPL。 他提到&#xff0c;尽管Java SE 9中有很多重要功能 &#xff0…

sinaapp mysql连接_手把手教你在新浪云上免费部署自己的网站--连接数据库

看完之后&#xff0c;默认你知道怎么将代码上传到新浪云SAE&#xff0c;并且能够成功运行&#xff0c;连接数据库之前&#xff0c;你必须先创建有一个应用。现在我创建一个名称为sampleone的应用&#xff0c;如下图点击左侧的代码管理&#xff0c;选在右侧创建一个版本然后就会…