4 ms·
This article focuses about memory, but even if memory wasn't an immediate issue (e.g. if you had smaller pages and didn't restrict the heap size to 1GB), you st
by unscaled 1y ago
This article focuses about memory, but even if memory wasn't an immediate issue (e.g. if you had smaller pages and didn't restrict the heap size to 1GB), you still want backpressure in this case.
What really happens here is that we've got two operations:
1. An I/O-bound operation (downloading URLs).
2. A CPU-bound operation (processing downloaded URLs).
Memory is a limited resource that requires you to introduce backpressure into the system, but so is the CPU. If downloads were fast and memory usage was relatively low (compared to the heap size), this program would have run into another issue: Thousands of lightweight threads all competing for the same small number of CPU cores when processing their downloaded data.
In practice, in most cases CPU-bound code in virtual threads would just introduce a new bottleneck, without proper back pressure. If the processing doesn't have any further I/O operations involved, the virtual threads will keep queuing up until the physical threads will finish processing everything (Java virtual threads, unlike goroutines in Go, cannot be preempted by the scheduler). This will just create more opportunities for an OOM, with virtual threads that keep piling up and waiting to be processed.
But even if you introduce a backpressure mechanism like a semaphore or BlockingQueue when queuing up the URLs, things can go bad for you here if your app has to do other things besdies processing URLs.
For instance, assume you've got a batch of 3000 URLs to process and each of these URLs takes 10 seconds to process. Let's say you're running everything on a CPU with 8 logical cores. Even if fully utilizing all of your CPU cores, you'll need about an hour to process everything. During that time any other I/O bound task that you do may be starved, because each time a URL processing task gets a hang of a carrier thread (the actual OS thread running virtual threads), it blocks it until processing is finished, and no other virtual threads can be scheduled on it! So in the scenario described here, you might quickly run out of carrier threads and all other I/O-bound operations would starve out waiting for available carrier threads.
If you've got many long-running CPU-bound tasks, it's often better to keep a traditional dedicated thread pool (ExecutorService.newFixedThreadPool()) just for these tasks.