问题
I have a Rust async server based on the Tokio runtime. It has to process a mix of latency-sensitive I/O-bound requests, and heavy CPU-bound requests.
I don't want to let the CPU-bound tasks monopolize the Tokio runtime and starve the I/O bound tasks, so I'd like to offload the CPU-bound tasks to a dedicated, isolated threadpool (isolation is the key here, so spawn_blocking
/block_in_place
on one shared threadpool are insufficient). How can I create such a threadpool in Tokio?
A naive approach of starting two runtimes runs into an error:
thread 'tokio-runtime-worker' panicked at 'Cannot start a runtime from within a runtime. This happens because a function (like
block_on
) attempted to block the current thread while the thread is being used to drive asynchronous tasks.'
use tokio; // 0.2.20
fn main() {
let mut main_runtime = tokio::runtime::Runtime::new().unwrap();
let cpu_pool = tokio::runtime::Builder::new().threaded_scheduler().build().unwrap();
let cpu_pool = cpu_pool.handle().clone(); // this is the fix/workaround!
main_runtime.block_on(main_runtime.spawn(async move {
cpu_pool.spawn(async {}).await
}))
.unwrap().unwrap();
}
Can Tokio allow two separate runtimes? Is there a better way to create an isolated CPU pool in Tokio?
回答1:
While Tokio already has a threadpool, the documentation of Tokio advises:
If your code is CPU-bound and you wish to limit the number of threads used to run it, you should run it on another thread pool such as rayon. You can use an oneshot channel to send the result back to Tokio when the rayon task finishes.
So, if you want to create a threadpool to make heavy use of CPU, a good way is to use a crate like Rayon and send the result back to the Tokio task.
回答2:
Starting a Tokio runtime already creates a threadpool. The relevant options are
- Builder::core_threads (default in 0.2.20 is the number of CPU cores)
- Builder::max_threads (default in 0.2.20 is 512)
Roughly speaking, core_threads
controls how many threads will be used to process asynchronous code. max_threads
- core_threads
is how many threads will be used for blocking work (emphasis mine):
Otherwise as
core_threads
are always active, it limits additional threads (e.g. for blocking annotations) asmax_threads
-core_threads
.
You can also specify these options through the tokio::main attribute.
You can then annotate blocking code with either of:
- task::spawn_blocking
- task::block_in_place
See also:
- What is the best approach to encapsulate blocking I/O in future-rs?
spawn_blocking
can easily take all of the threads available in the one and only runtime, forcing other futures to wait on them
You can make use of techniques like a Semaphore to restrict maximum parallelism in this case.
回答3:
So it seems that the obvious way of creating another runtime should work, and the error is likely just a bug in Tokio
The workaround is to use Handle
, not Runtime
directly, for spawning tasks on the other runtime.
fn main() {
let mut main_runtime = tokio::runtime::Runtime::new().unwrap();
let cpu_pool = tokio::runtime::Builder::new().threaded_scheduler().build().unwrap();
// this is the fix/workaround:
let cpu_pool = cpu_pool.handle().clone();
main_runtime.block_on(main_runtime.spawn(async move {
cpu_pool.spawn(async {}).await
}))
.unwrap().unwrap();
}
来源:https://stackoverflow.com/questions/61752896/how-to-create-a-dedicated-threadpool-for-cpu-intensive-work-in-tokio