我继承了一些代码,并没有原始开发人员离开。代码使用了很多CompletableFuture
,这是我第一次使用它,所以我仍然试图绕过它。据我了解,(Completable)Future
通常与一些多线程机制一起使用,这将允许我们在执行耗时的任务时执行其他操作,然后通过Future获取其结果。与the javadoc中一样:
interface ArchiveSearcher { String search(String target); }
class App {
ExecutorService executor = ...
ArchiveSearcher searcher = ...
void showSearch(final String target) throws InterruptedException {
Future<String> future = executor.submit(new Callable<String>() {
public String call() {
return searcher.search(target);
}});
displayOtherThings(); // do other things while searching
try {
displayText(future.get()); // use future
} catch (ExecutionException ex) { cleanup(); return; }
}
}
但是,在我继承的这个应用程序中,以下不使用任何多线程的模式出现了很多次:
public Object serve(Object input) throws ExecutionException, InterruptedException {
CompletableFuture<Object> result = delegate1(input);
return result.get();
}
private CompletableFuture<Object> delegate1(Object input) {
// Do things
return delegate2(input);
}
private CompletableFuture<Object> delegate2(Object input) {
return CompletableFuture.completedFuture(new Object());
}
对我而言,这相当于:
public Object serve(Object input) {
Object result = delegate1(input);
return result;
}
private Object delegate1(Object input) {
// Do things
return delegate2(input);
}
private Object delegate2(Object input) {
return new Object();
}
当然代码要复杂得多,如果出现错误会返回exceptionallyCompletedFuture
,但有Callable
,没有Runnable
,没有Executor
,没有{ {1}}没有多线程的迹象。我错过了什么?在单线程上下文中使用supplyAsync()
有什么意义?
答案 0 :(得分:2)
期货对于存在异步编程的情况至关重要。异步编程的一大优势是它允许您使用单个线程编写非常高效的代码。
此外,期货往往是一个全有或全无的主张。如果你想编写异步代码,你必须从上到下进行编写,即使并非每个方法都是异步的。
例如,假设您要编写单个线程HTTP服务器,如twisted或express。服务器的顶层(这里非常宽松的伪代码)可能类似于:
while (true) {
if (serverSocket.ready()) {
connection = serverSocket.accept();
futures.add(server.serve(connection));
}
for (Future future : futures) {
if (future.isDone()) {
Object result = future.get();
sendResult(result);
}
}
//Some kind of select-style wait here
}
只有一个线程,但是任何时候发生通常需要等待的操作(从数据库,文件中读取,在请求中读取等),它使用期货并且不会阻止一个线程,所以你拥有一个高性能的单线程HTTP服务器。
现在,想象一下,如果您的应用程序的最高级别与上述类似,会发生什么情况,并且在某些时候,某些非常低级别的请求必须从文件中读取某些内容。该文件读取将产生未来。如果你们之间的所有中间层都没有处理未来,那么你将不得不阻止并且它会破坏目的。这就是为什么我说期货往往是全有或全无。
所以我猜是:
答案 1 :(得分:1)
是的,目前在该代码中没有使用多线程。看起来有意编写单线程代码,如果开发人员以后决定使用多线程,那么
delegate2()
方法应该被修改。
答案 2 :(得分:0)
ExecutorService实现通常管理线程。我使用过ThreadPoolExecutor,它就是这样做的。您注释掉了代码使用的ExecutorService。
答案 3 :(得分:0)
异步代码的要点是推迟连续代码。
最常见的情况是I / O,而不是等待操作完成,你说&#34;做你的事情并在你完成时通知我#34;或者更常见的是& #34;做你的事情并在你完成时做到这一点&#34;。
这根本不暗示线程。从任何设备读取,无论是网卡还是硬盘驱动器,通常都会从设备向CPU发送某种信号或中断。您可以在此期间使用CPU。 &#34;通知我&#34;在较低级别的代码中更常见,您可以在其中实现调度循环或调度程序; &#34;做这个&#34;在更高级别的代码中更常见,您可以使用已为您调度和/或计划的已建立的库或框架。
不常见的场景包括推迟执行而不阻塞线程(考虑计时器与Thread.sleep()
)和拆分工作。实际上,拆分工作在多个线程中非常常见,你可以通过一些开销来提高性能,但对于单个线程来说并不是那么多,其中开销只是开销。
您提供的代码仅作为构建完成CompletableFuture
的示例,无论是成功还是异常,都是异步代码开销的一部分,并非真正异步。也就是说,您仍然必须遵循定义的异步样式,在这种情况下,即使您可以立即提供结果,也需要为结果分配少量内存。
这可能会在每秒数千个呼叫中变得明显,或者每个线程每秒有数百个呼叫,并且有数十个线程。
有时,您可以通过预定义的已完成期货进行优化,例如: null
,0
,1
,-1
,空数组/列表/流,或您在域中可能具有的任何其他非常常见或甚至是固定的结果。类似的方法是缓存包装未来,而不仅仅是结果,而结果保持不变。但是我建议你在走这条路之前首先进行简介,你最终可能会过早地优化一些很可能不是瓶颈的东西。