ARTS 打卡第十五周(2023.11.20~2023.11.26)

104 阅读5分钟

1. Algorithm 每周一道算法题

本周算法题是最长递增自序列

本题的关键在于使用动态规划解题时,每次计算当前位置最长递增子序列长度时,需要从头到当前位置为止进行便利计算得出,这种解法耗时较长,有一种是使用二分插入+动态规划的方式解决问题,但是该方式的原理还不太懂,后续有时间可以研究下

2. Review 阅读一篇英文文章

本周继续阅读Virtual Threads

Represent Every Concurrent Task as a Virtual Thread; Never Pool Virtual Threads

The hardest thing to internalize about virtual threads is that, while they have the same behavior as platform threads they should not represent the same program concept.

使用虚拟线程最难的是在他们和平台级线程拥有相同的表现时,却不能用相同的编程概念来表示虚拟线程

Platform threads are scarce, and are therefore a precious resource. Precious resources need to be managed, and the most common way to manage platform threads is with thread pools. A question that you then need to answer is, how many threads should we have in the pool?

平台级线程是稀缺的并且是珍贵的资源。珍贵的资源需要被妥善的管理,最常见的管理平台级线程的方法是使用线程池。你需要知道的问题是,线程池中有多少线程才是合理的?

But virtual threads are plentiful, and so each should represent not some shared, pooled, resource but a task. From a managed resource threads turn into application domain objects. The question of how many virtual threads we should have becomes obvious, just as the question of how many strings we should use to store a set of user names in memory is obvious: The number of virtual threads is always equal to the number of concurrent tasks in your application.

但是虚拟线程是充足的,并且因此不应该是共享的,池化的的资源,而应该是一个任务。从托管资源的角度看,线程转化为应用程序域对象。我们应该使用多少虚拟线程就变得非常明显了,就跟我们需要使用多少 string 对象在内存中保存一系列的用户名一样明显:虚拟线程的数量总是应该和应用程序并发任务数相同。

Converting n platform threads to n virtual threads would yield little benefit; rather, it's tasks that need to be converted.

将 n 个平台级线程替换为虚拟线程收益是非常小的;应该将每个任务转换为一个虚拟线程。

To represent every application task as a thread, don't use a shared thread pool executor like in the following example:

为了将每个应用程序任务转换为一个线程, 不要像下面一样使用共享的线程池:

Future<ResultA> f1 = sharedThreadPoolExecutor.submit(task1);
Future<ResultB> f2 = sharedThreadPoolExecutor.submit(task2);
// ... use futures

Instead, use a virtual thread executor like in the following example:

相反,应该像下面例子一样使用虚拟线程来进行替换:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
   Future<ResultA> f1 = executor.submit(task1);
   Future<ResultB> f2 = executor.submit(task2);
   // ... use futures
}

The code still uses an ExecutorService, but the one returned from Executors.newVirtualThreadPerTaskExecutor() doesn't employ a thread pool. Rather, it creates a new virtual thread for each submitted tasks.

代码中仍旧使用 ExecutorService,但是 Executors.newVirtualThreadPerTaskExecutor() 方法返回的不是一个线程池,它为每个提交的任务创建一个虚拟线程。

Furthermore, that ExecutorService itself is lightweight, and we can create a new one just as we would with any simple object. That allows us to rely on the newly added ExecutorService.close() method and the try-with-resources construct. The close method, that is implicitly called at the end of the try block will automatically wait for all tasks submitted to the ExecutorService—that is, all virtual threads spawned by the ExecutorService—to terminate.

此外,ExecutorService 本身是轻量的,我们可以像创建任何一个简单对象一样创建一个新的 ExecutorService。这使得我们能够依赖新添加的 ExecutorService.close() 方法和 try-with-resources 结构。close 方法会在 try 块的末尾自动调用,并等待所有被提交的任务完成,也就是等待所有 ExecutorService 创建的虚拟线程终止。

This is a particularly useful pattern for fanout scenarios, where you wish to concurrently perform multiple outgoing calls to different services like in the following example:

这在 fanout 场景下是很有用的方式,当你希望同时对不同服务进行多个出站调用,就像下面示例一样:

void handle(Request request, Response response) {
    var url1 = ...
    var url2 = ...
 
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        var future1 = executor.submit(() -> fetchURL(url1));
        var future2 = executor.submit(() -> fetchURL(url2));
        response.send(future1.get() + future2.get());
    } catch (ExecutionException | InterruptedException e) {
        response.fail(e);
    }
}
 
String fetchURL(URL url) throws IOException {
    try (var in = url.openStream()) {
        return new String(in.readAllBytes(), StandardCharsets.UTF_8);
    }
}

You should create a new virtual thread, as shown above, for even small, short-lived concurrent tasks.

你应该像上面一样创建一个虚拟线程,对于甚至更小,生命周期更短的并发任务也一样。

For even more help writing the fanout pattern and other common concurrency patterns, with better observability, use structured concurrency.

为了更好地帮助编写 fanout 模式和其他常见的并发模式,并提供更好的观测能力,可以使用结构化并发。

As a rule of thumb, if your application never has 10,000 virtual threads or more, it is unlikely to benefit from virtual threads. Either it experiences too light a load to need better throughput, or you have not represented sufficiently many tasks to virtual threads.

一般来说,如果你的应用从不使用超过 10000 个虚拟线程,似乎使用虚拟线程收益不大。要么它负载过轻以至于不需要更高的吞吐量,要么你没有将足够的任务分配给虚拟线程。

3. Techeniques/Tips 分享一个小技巧

本周在学习 go 的知识,学习了方法,发现跟 Java 其实很类似,go 中的方法接收者分为值类型接收者、指针类型接收者,初学者很容易将之搞混,分不清他们之间的区别,但是换个角度看问题就一目了然了,将接收者当做方法第一个参数来看待,值类型接收者参数在方法调用时会复制值本身,因此对其的任意操作都是对复制对象的操作,指针类型接收者会复制指针作为第一个参数传入,因此对其的修改能影响到对象本身,对比 Java 来看,其实此处的接收者就是 Java 方法的 this 参数,这样一对比,一下的清清楚楚了

4. Share 分享一个观点

语言是相通的,说得不是语言的特点是一样的,而是语言的设计思想是一致的,掌握了某个语言的设计思想,学习其他语言时,对比设计思想来学习,往往能达到事半功倍的效果,最近在学习 Go,我经常对照 Java 来进行学习,发现效果出奇的好。