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

Urb*_*Urb 4 java inputstream nonblocking thread-local java-21

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

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

也就是说,读取是InputStream阻塞IO操作吗?

请记住,响应可能非常大,可能包含超过 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);
        }
    }
} 
Run Code Online (Sandbox Code Playgroud)

Hol*_*ger 5

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

\n

异步方法必须返回一个Future或类似类型的 Promise 对象,该对象允许它们在计算实际结果之前返回。这就要求调用者能够处理这种结果。当调用者必须将完整构造的结果返回给调用者时,它会让您回到第一个位置,因为它再次需要阻塞等待。

\n

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

\n

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

\n
List<String> straightForward() throws IOException {\n    try(InputStream bigInputStream = getResponse()) {\n        return deserialize(bigInputStream);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

它\xe2\x80\x99是调用者\xe2\x80\x99的责任在虚拟线程中调用您的代码,以获得好处。或者,也许\xe2\x80\x99s 更好的说法是\xe2\x80\x99s 调用者\xe2\x80\x99s 选择仅同步或在虚拟线程中使用你的方法。但调用者也可能以普通同步方式使用您的方法,但调用者将调用安排在虚拟线程中。

\n

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

\n

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