Reputation: 410
I am migrating my Spring Boot application to virtual threads from the platform ones. Generally, it works fine, no pinning happens. But for some requests, there could be an operation that leads to the pinning (due to a blocking operation inside of a synchronized block) and I want to avoid that. If I don't, I could end up in a situation when all the carriers are pinned and there's no platform thread to serve a request left.
The operation that leads to the pinning is a part of a 3rd-party library. While waiting for the fix that would allow me to execute the library's code without pinning, I am planning to use the following approach:
Submit the code that pins the virtual thread's carrier to an executor service (backed by platform threads) and immediately call .get()
(from a virtual thread) on the CompletableFuture
I would receive from the executor.
Edited: Here is the demonstration code (Spring Boot)
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Component
class AppRunner implements ApplicationRunner {
private final Object monitor = new Object();
private final ExecutorService executorService;
public AppRunner() {
this.executorService = Executors.newFixedThreadPool(2);
}
@Override
public void run(ApplicationArguments args) {
String parallelismNum = System.getProperty("jdk.virtualThreadScheduler.parallelism");
System.out.println("jdk.virtualThreadScheduler.parallelism is set to: " + parallelismNum);
try (var virtualExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int taskId = 0; taskId < 100; taskId++) {
final int taskIdentifier = taskId;
System.out.println("Submitting the task to executor: " + taskIdentifier);
virtualExecutor.submit(() -> simpleLongOperation(taskIdentifier));
/**
* If I submit the following instead of pinningOperationOnPlatform,
* My simpleLongOperation would be waiting for a carrier to unpin
*/
// virtualExecutor.submit(() -> pinningOperation(taskIdentifier));
virtualExecutor.submit(() -> pinningOperationOnPlatform(taskIdentifier));
}
}
}
/**
* This runs the pinning operation on a platform thread but awaits on the virtual.
* So no pinning is actually happening.
*/
private void pinningOperationOnPlatform(int id) {
try {
executorService.submit(() -> pinningOperation(id))
.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void simpleLongOperation(int id) {
System.out.println("\tSIMPLE Start operation " + id + " : " + Instant.now());
sleep(1_000);
System.out.println("\tSIMPLE Complete operation " + id + " : " + Instant.now());
}
private void pinningOperation(int id) {
System.out.println("PINNING Start operation " + id + " : " + Instant.now());
synchronized (monitor) {
sleep(5_000);
}
System.out.println("PINNING Complete pinning operation " + id + " : " + Instant.now());
}
private void sleep(long millis) {
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
As I understand, the operation could now wait to enter the synchronized block on the executor's pool thread, but since the .get()
method is called from a virtual thread, their carrier would be free once the operation is done. Now it seems that I'm limiting my parallelism to the size of the thread pool the executor uses to move the pinning operation on the platform thread to run, but only for the (rare) requests that require the operation to run.
For me it seems like it wouldn't be bad for the application performance. Are my assumptions correct, or am I missing something?
Upvotes: 4
Views: 612
Reputation: 2348
Yes, if you invoke platform thread from virtual thread, via executor or anyhow else, the latter one cannot be pinned, of course, because pinning is only applicable to virtual threads. With this design, however, you make that platform thread unavailable for scheduling during the same time the virtual thread is pinned. Pinning is not a catastrophe, it does not (at least should not) lead to the hanging, deadlock or anything of that kind, it is an exclusion of carrier (platform) thread from scheduling - which is what this design is doing essentially: it excludes platform thread from scheduling while it awaits on Future.get()
in the line
executorService.submit(() -> pinningOperation(id)).get();
Therefore, like Naman correctly said, you won't gain much with such design, and I daresay - nothing at all, essentially a trade шилa на мыло.
Moreover, on the long run this design, being itself somehow convoluted, overengineered and not too simple to read, might appear less scalable. First of all, it is not clear does synchronized
block always cause pinning.
Having a synchronized alone does not have to cause pinning.
You said that you discovered that this piece of code always lead to pinning, but it may change in the later releases. In addition, virtual thread team promised to eliminate pinning on synchronized
block in near future. Alan Bateman wrote
The changes means that virtual threads don't pin their carrier when parking (doing socket I/O for example) while in a synchronized method,
The only excuse for such design is that pinning in a particular case leads to hanging. This behavior has been reported in several Stack Overflow threads, for example here or here. If this is a case, then one might consider to refrain from using virtual threads altogether or wait until this behavior will be eliminated in later releases.
Upvotes: 0