-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Threadpool blocking #317
Threadpool blocking #317
Conversation
This patch adds a `blocking` to `tokio-threadpool`. This function serves as a way to annotate sections of code that will perform blocking operations. This informs the thread pool that an additional thread needs to be spawned to replace the current thread, which will no longer be able to process the work queue.
tokio-threadpool/src/builder.rs
Outdated
@@ -255,6 +289,22 @@ impl Builder { | |||
self | |||
} | |||
|
|||
/// TODO: Dox |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
@@ -99,11 +103,14 @@ impl Builder { | |||
|
|||
Builder { | |||
pool_size: num_cpus, | |||
max_blocking: 100, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a reason this isn't stored on the Config
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Config
object is passed into the pool. Values not in Config
don't need to be stored and are just used to initialize state (in this case, populate the stack of sleeping threads).
tokio-threadpool/src/notifier.rs
Outdated
let t2 = t1.clone(); | ||
|
||
mem::forget(t1); | ||
let t1 = Forget::new(unsafe { Arc::from_raw(ptr) }); | ||
|
||
// t2 is forgotten so that the fn exits without decrementing the ref |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment still refers to t2
but that variable has been removed.
tokio-threadpool/src/pool/backup.rs
Outdated
use std::sync::atomic::Ordering::{self, Acquire, AcqRel, Relaxed}; | ||
|
||
#[derive(Debug)] | ||
pub(crate) struct Backup { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are Backup
's responsibilities? Why is it called Backup?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done... hopefully it makes sense.
Backup
isn't a great name though...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks
Ok, I think this should be ready to be merged. cc/ @olix0r and @seanmonstar |
When calling `blocking` from outside of a threadpool, error instead of panic.
I can look at the code a little later, but my first thought after reading the motivation is that it'd be much more convincing with measurements. Blah blah premature optimizations blah :) I'd be worried about a few things here:
A comparison of this new feature set versus a typical thread pool with a channel would be useful. More so two, one with occasional blocking calls and many smaller async tasks, and another that just slams blocking calls. |
I have not done any measurements personally, but I would argue for precedent. This is the strategy employed by both Go, Erlang, and Java (
No, none of the code paths / logic impact normal usage. They all happen once you call
I'm not sure I follow exactly the question, but there should be no impact. This added logic does not impact the ability to steal tasks from the worker. Also, once a worker enters blocking mode, its work queue is handed off to thread standing by, so there is minimal lag in processing the work queue of the worker that just transitioned to blocking mode. |
I don't think benchmarks at this point would be super useful as I have not put a huge emphasis on tuning for max performance. Again, at this point I am heavily leaning on precedent and established best practice and getting the features implemented so that there is a solid base upon which tuning can be done. |
Also, there is the fact that, w/ You could, of course, move greater amounts of logic from your task to the blocking thread pool, but then you start losing the advantages of the runtime. I guess, this is all to say, I'm not sure what a fair comparison of this strategy vs. a channel would be. PS: I have no idea why AppVeyor is unable to download Rust. |
What I mean is if that thread had many async tasks to poll, entering blocking mode means message passing to steal the tasks, and those other workers may have already been loaded. Worse, if the blocking operation didn't actually block (say the file was in cache), then this worker needs to spend time stealing tasks back immediately. I just have a perhaps wrongly placed fear that usage of this could have more costs than saved, in some cases, and I'm scared of giving that power to a user. The fact that everywhere in Scala docs around async stuff has gigantic warnings to not block means users will mess it up anyways. As for no measurements, I know they aren't here yet, hence why I said seeing them would be more convincing. In many projects, especially where the highest performance is required, measurements are essential for proving fundamental changes. I don't maintain Tokio, so of course I couldn't demand them. I would however require them before using them in hyper. 🤷 |
Ah no, that isn't at all what happens.
The hand off process is just an atomic flag set to signal the hand off and a condvar signal if the idle thread is sleeping. At this point, it becomes the old worker thread and continues processing the work queue from the point the old thread left off. The entire hand off process is allocation free and completes and is not dependent on the number of pending tasks. Re numbers, it would be trivial to show a comparison that significantly favors this strategy. I don't know if it would be worth while. For example, repeatedly reading
vs.
If you think this is worthwhile, I can do it. Part of the win of |
tokio-threadpool/src/pool/backup.rs
Outdated
/// token (`WorkerId`) is handed off to that running thread. If none are found, | ||
/// a new thread is spawned. | ||
/// | ||
/// This state is manages the exchange. A thread that is idle, not assigned to a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"This state manages the exchange"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
generally lgtm! a few clarity comments left.
i've been running tests in a loop for most of the past 24 hours without any failures.
tokio-threadpool/src/pool/backup.rs
Outdated
use std::sync::atomic::Ordering::{self, Acquire, AcqRel, Relaxed}; | ||
|
||
#[derive(Debug)] | ||
pub(crate) struct Backup { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks
/// | ||
/// * Performing synchronous file operations (reading and writing). | ||
/// * Blocking on acquiring a mutex. | ||
/// * Performing a CPU bound computation, like cryptographic encryption or |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"CPU-bound"
tokio-threadpool/src/builder.rs
Outdated
/// Set the maximum number of concurrent blocking sections. | ||
/// | ||
/// This must be a number between 1 and 32,768 though it is advised to keep | ||
/// this value on the smaller side. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens when the number of blocking
calls exceeds max_blocking
? Should be called out here.
|
||
/// Execute function `f` before each thread stops. | ||
/// | ||
/// This is intended for bookkeeping and monitoring use cases. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Presumably this cannot be guaranteed to run (whereas after_start
is guaranteed to run before work can be scheduled?). Maybe worth mentioning explicitly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the thread exits, it should run (as long as the threadpool lib itself doesn't panic).
What sort of guarantee would you expect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
basically, i'm wondering how a process needs to manage the threadpool during process shutdown. Servers typically are waiting on some set of futures that complete once the process has received a shutdown signal. Once these futures complete, is there a way to shutdown the pool gracefully? If the pool is just dropped, are threads guaranteed to exit cleanly before the main thread exits?
to confirm:
This means that timers/reactors/etc that have thread-local state are migrated as part of this, correct? |
@seanmonstar I guess my question is what do you want to measure and what do you hope to learn? |
@seanmonstar I committed a benchmark that tests
And nothing has been tuned on my end yet. I would expect the improvements be even more pronounced depending on the use case. A big part of the win of using |
@olix0r Yes, the timer, reactor, etc... (i.e, the |
Should the |
@jonhoo I'm actually thinking that |
tokio/tokio-threadpool/src/worker/mod.rs Lines 199 to 201 in 8d8c895
When is this implemented? |
It is not implemented but is not critical to the execution. It would be a perf improvement. |
This patch adds a
blocking
totokio-threadpool
. This function serves as away to annotate sections of code that will perform blocking operations. This
informs the thread pool that an additional thread needs to be spawned to
replace the current thread, which will no longer be able to process the work
queue.
By default, the Tokio thread pool expects that tasks will only run for short
periods at a time before yielding back to the thread pool. This is the basic
premise of cooperative multitasking.
However, it is common to want to perform a blocking operation while
processing an asynchronous computation. Examples of blocking operation
include:
decryption.
One option for dealing with blocking operations in an asynchronous context
is to use a thread pool dedicated to performing these operations. This not
ideal as it requires bidirectional message passing as well as a channel to
communicate which adds a level of buffering.
Instead,
blocking
hands off the responsiblity of processing the work queueto another thread. This hand off is light compared to a channel and does not
require buffering.