很久以前,想要在Java中同时运行代码时,必须手动启动新线程。 这不仅很难编写,而且很容易引入难以发现的错误。 测试,阅读和维护此类代码也不是一件容易的事。 从那时起-有了来自多核计算机的一点激励,Java API不断发展,使并发代码的开发更加容易。 同时,其他JVM语言也对帮助开发人员编写此类代码有其见解。 在本文中,我将比较它在Java和Kotlin中的实现方式。
为了使文章重点突出,我特意省略了性能,以撰写有关代码可读性的文章。
关于用例
用例不是很原始。 我们需要调用不同的Web服务。 天真的解决方案是依次调用它们,一个接一个,然后收集每个结果。 在那种情况下,总的呼叫时间将是每个服务的呼叫时间的总和。 一个简单的改进是并行调用它们,并等待最后一个完成。 因此,性能从线性提高到常数-或更精确地说,从o(n)提高到o(1) 。
为了模拟延迟地调用Web服务,让我们使用以下代码(在Kotlin中使用,因为它不那么冗长):
classDummyService(privatevalname:String){
privatevalrandom=SecureRandom()
valcontent:ContentDuration
get(){
valduration=random.nextInt(5000)
Thread.sleep(duration.toLong())
returnContentDuration(name,duration)
}
}
dataclassContentDuration(valcontent:String,valduration:Int)
Java Future API
Java提供了一个完整的类层次结构来处理并发调用。 它基于以下类别:
-
可召回
-
Callable
是“返回结果的任务”。 从另一个角度来看,它类似于不带任何参数并返回此结果的函数。
未来
-
Future
是“异步计算的结果”。 另外,“只能在计算完成时使用get
方法检索结果,必要时将阻塞直到准备就绪为止”。 换句话说,它表示一个值的包装,其中该值是计算的结果。
执行人服务
-
ExecutorService
“提供了用于管理终止的方法以及可以生成用于跟踪一个或多个异步任务进度的Future
方法”。 它是Java中并发处理代码的入口点。 可以通过Executors
类中的静态方法来获得此接口的实现以及更为专门的接口。
以下类图对此进行了总结:
使用并发包致电我们的服务需要两个步骤。
创建可调用对象的集合
首先,需要有一组Callable
才能传递给执行者服务。 这可能是这样的:
- 来自服务名称流
- 对于每个服务名称,创建一个新的虚拟服务,该服务以字符串初始化
- 对于每个服务, 以
Callable
返回服务的getContent()
方法引用 。 之所以Callable
是因为方法签名与Callable.call()
相匹配,并且Callable
是一个功能接口。
这是准备阶段。 它将转换为以下代码:
List<Callable<ContentDuration>>callables=Stream.of("Service A","Service B","Service C")
.map(DummyService::new)
.map(service->(Callable<ContentDuration>)service::getContent)
.collect(Collectors.toList());
处理可调用项
清单准备好之后,就该由ExecutorService
来处理它了, 也就是“实际工作”。
- 创建一个新的执行者服务-任何都可以
- 将
Callable
的列表传递给执行者服务,并流式传输Future
的结果列表 - 对于每个未来,
- 要么返回结果
- 或处理异常
以下代码段是可能的实现:
ExecutorServiceexecutor=Executors.newWorkStealingPool();
List<ContentDuration>results=executor.invokeAll(callables).stream()
.map(future->{
try{returnfuture.get();}
catch(InterruptedException|ExecutionExceptione){thrownewRuntimeException(e);}
}).collect(Collectors.toList());
Future API,但在Kotlin中
让我们面对现实吧,尽管Java使得编写并发代码成为可能,但是读取和维护它并不是那么容易,主要是因为:
- 在集合和流之间来回移动
- 在Lambda中处理检查的异常
- 显式投射
只需将上面的代码移植到Kotlin即可消除这些限制并使之更加简单:
varcallables:List<Callable<ContentDuration>>=arrayOf("Service A","Service B","Service C")
.map{DummyService(it)}
.map{Callable<ContentDuration>{it.content}}
valexecutor=Executors.newWorkStealingPool()
valresults=executor.invokeAll(callables).map{it.get()}
Kotlin协程
随着Kotlin 1.1版的推出,新的实验功能称为协程 。
基本上,协程是可以在不阻塞线程的情况下挂起的计算。 阻塞线程通常很昂贵,尤其是在高负载下。 另一方面,协程悬架几乎是免费的。 不需要上下文切换或操作系统的任何其他参与。
协程背后的主要设计原则是,它们必须感觉像顺序代码,但必须像并发代码一样运行。 它们基于以下(简化的)类图:
但是,没有什么比代码本身更好。 让我们实现与上面相同的方法,但是在Kotlin中使用协程而不是Java期货。
首先,让我们扩展服务,以通过添加围绕类型为Deferred
content
的新计算属性来简化进一步处理:
valDummyService.asyncContent:Deferred<ContentDuration>
get()=async(CommonPool){content}
这是标准的Kotlin扩展属性代码,但请注意CommonPool
参数。 这就是使代码并发运行的魔力。 它是一个伴随对象( 即单例),它使用多重后备算法来获取ExecutorService
实例。
现在,继续正确的代码流:
- 协程在一个块内处理。 在要在其中分配的块外声明一个变量列表
- 打开同步块
- 创建服务名称数组
- 为每个名称创建一个服务并返回
- 对于每个服务,获取其异步内容(如上所述)并返回
- 对于每个延迟的结果,将其返回并返回
// Variable must be initialized or the compiler complains
// And the variable cannot be used afterwards
varresults=runBlocking{
arrayOf("Service A","Service B","Service C")
.map{DummyService(it)}
.map{it.asyncContent}
.map{it.await()}
}
外卖
除了Java语言本身之外,Future API并不是什么大问题。 一旦将代码翻译成Kotlin,可读性就会大大提高。 但是,必须创建一个集合以传递给执行程序服务,这会破坏功能管道。
在协程方面,请记住它们仍处于实验阶段。 尽管如此,代码的确看起来是顺序的,因此更具可读性,并且表现出并行性。
翻译自: https://blog.frankel.ch/concurrency-java-futures-kotlin-coroutines/