用于从 HTTP 响应读取 InputStream 的虚拟线程

Virtual thread for reading InputStream from HTTP response

提问人:Urb 提问时间:10/6/2023 最后编辑:ProgmanUrb 更新时间:10/6/2023 访问量:198

问:

使用 java 21,只需在虚拟线程中执行即可将阻塞 IO 代码转换为非阻塞 IO 代码。

我应该简单地包装返回的 HTTP 调用(如在方法中),还是在虚拟线程中执行读取和反序列化(如在方法中)会更有效?InputStreamnonBlockingAInputStreamnonBlockingB

换言之,读取是否阻塞 IO 操作?InputStream

请记住,响应可能非常大,可能包含超过 500,000 个字符串。 我也不确定使用的库是否使用任何 ThreadLocals,不建议使用虚拟线程


@SuppressWarnings("unchecked")
class Request {
    private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
    private CloseableHttpClient httpApacheClient;

    List<String> nonBlockingA() throws Exception {
        InputStream bigInputStream = executorService.submit(this::getResponse).get();
        return deserialize(bigInputStream);
    }

    List<String> nonBlockingB() throws Exception {
        return executorService.submit(() -> {
            InputStream bigInputStream = getResponse();
            return deserialize(bigInputStream);
        }).get();
    }

    private InputStream getResponse() throws IOException {
        return httpApacheClient.execute(new HttpGet("http://random/names/size/500000")).getEntity().getContent();
    }

    private static List<String> deserialize(InputStream body) throws IOException {
        try (InputStreamReader reader = new InputStreamReader(body, UTF_8)) {
            return new Gson().fromJson(reader, List.class);
        }
    }
} 
java inputstream 非阻塞 线程本地 java-21

评论

4赞 Mark Rotteveel 10/6/2023
使用虚拟线程并不能使代码不阻塞,它只是意味着载体线程可用于执行其他工作。
0赞 Urb 10/6/2023
@MarkRotteveel好吧,你是对的。但我的问题仍然存在。哪种方法更好/
0赞 M. Deinum 10/6/2023
所有方法都被阻塞了,因为你正在调用 .包装所有(或使用使用虚拟线程的执行器)不会使其突然变得不阻塞。如果你想让它不阻塞,你需要在整个堆栈中使用一个。正如 Mark 所说,如果你使用虚拟线程,实际的平台线程将能够完成其他工作。get()CompletableFuture

答:

4赞 Holger 10/6/2023 #1

您的变体之间没有显着差异。任何返回完全构造结果的方法(如 )始终是同步或阻塞方法,因为无法等待异步或外部计算的任何所需结果来构造最终结果。List<String>

异步方法必须返回一种或类似类型的 promise 对象,这允许它们在计算实际结果之前返回。这要求调用方能够处理这种结果。当调用方必须将完全构造的结果返回给调用方时,它会将您带回原点,因为它再次需要阻塞等待。Future

因此,这种传统的异步处理要求所有处理步骤都实现为异步操作,链接回调以将实际计算推迟到输入可用时。

虚拟线程的要点在于,它们根本不需要您编写这种传统的异步方法。您可以以同步方式编写操作。为了保持你的例子,

List<String> straightForward() throws IOException {
    try(InputStream bigInputStream = getResponse()) {
        return deserialize(bigInputStream);
    }
}

调用方有责任在虚拟线程中调用您的代码,以获得好处。或者,也许最好说调用者可以选择使用您的方法,只是同步或在虚拟线程中使用。但是,调用方也有可能以简单的同步方式使用您的方法,但调用方的调用方在虚拟线程中安排了调用。

在最好的情况下,99% 的代码不处理线程或异步 API,而是以同步方式直接编写。只有剩下的 1% 必须在虚拟线程中安排调用,例如 Web 服务器为每个请求创建一个虚拟线程或类似的东西。

评论

3赞 Holger 10/6/2023
这毫无意义。如果您怀疑该操作阻塞了载体线程,那么在另一个虚拟线程中启动它就没有意义了,因为虚拟线程也会阻塞载体线程。你把事情搞得太复杂了。如果底层 I/O 操作已调整,它将释放载体线程,无需担心。如果没有,它将以一种或另一种方式阻塞平台线程,并且您对此无能为力。同样,您也无法改变实际计算必须在 CPU 内核上的平台线程上运行的事实。因此,只需使用直接的同步形式即可。
2赞 Holger 10/6/2023
反之亦然。如果代码使用线程局部变量并且不清理它们,则传统线程池存在内存泄漏。当您为每个请求使用虚拟线程时,所有线程局部变量肯定会在请求处理结束时死亡。再说一次,你把事情搞得太复杂了。为 I/O 操作启动另一个虚拟线程没有任何好处。
2赞 Holger 10/6/2023
然后,如前所述,您将无法从虚拟线程中受益。获得好处的唯一方法是以预期的方式使用它们。无论哪种情况,无论您的呼叫威胁是否是虚拟的,在这种情况下启动新的虚拟线程都没有好处。你的方法就像,在听说面包车比手推车好之后,把所有东西都放进面包车里,试着像手推车一样拉它。
3赞 Holger 10/6/2023
是的,在虚拟线程中运行 100 个提取操作是有意义的。“每次提取一个虚拟线程”方法类似于 Web 服务器示例中的“每个请求一个虚拟线程”。启动所有提取并等待结果的代码是必须处理虚拟线程的 1% 的代码,而执行实际提取操作的代码可以像同步/阻塞代码一样编写。
2赞 Holger 10/6/2023
尽可能使用最简单的实现,因为这就是此功能的全部意义所在,能够编写简单的代码。请记住,第三方库(通常)会选择JDK团队已经改编或即将改编的JDK方法,因此,即使第三方库尚未改编,在I/O上释放载体线程的能力也已经存在。关于线程局部变量,不要高估它们的影响。这只是一个性能问题,如果有的话,所以试着衡量一下......