-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
[RFC] Add #[diagnostic::blocking]
attribute
#3639
base: master
Are you sure you want to change the base?
Conversation
`#[diagnostic::blocking]` is a marker attribute for functions that are considered (by their author) to be a blocking operation, and as such shouldn't be invoked from an `async` function. `rustc`, `clippy` and other tools can use this signal to lint against calling these functions in `async` contexts. It would allow `rustc`, `clippy` and other tools to provide lints like ``` warning: async function `foo` can block --> $DIR/blocking-calls-in-async.rs:28:1 | 28 | async fn foo() { | -------------- 29 | interesting(); | ^^^^^^^^^^^^^`foo` is determined to block because it calls into `interesting` | note: `interesting` is considered to be blocking because it was explicitly marked as such --> $DIR/blocking-calls-in-async.rs:5:1 | 5 | #[diagnostic::blocking] | ^^^^^^^^^^^^^^^^^^^^^^^ 6 | fn interesting() {} | ---------------- ```
a3d24bb
to
d3afb4c
Compare
How does this interact with RFC #3014? Isn't Would like to see that mentioned in the RFC -- if it's similar enough, then this RFC can just subsume that one. |
The |
As Estaban said, they're kinda related. For example, |
Yeah, I think the main distinction is that values must not suspend, but calls are blocking. I am curious if these can be unified futher, but worst case |
It might be that this annotation on functions could influence the logic of the
I agree. |
#[diagnostic::blocking]
attribute#[diagnostic::blocking]
attribute
what functions besides
|
# Unresolved questions | ||
[unresolved-questions]: #unresolved-questions | ||
|
||
- What parts of the design do you expect to resolve through the RFC process before this gets merged? | ||
- What parts of the design do you expect to resolve through the implementation of this feature before stabilization? | ||
- What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC? |
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.
fill out or delete the placeholder text?
Effectively most of |
As a semi related note: As far as I remember one of the main motivations for the specification of diagnostic attribute namespaces was to outline a set of common rules for all attributes in this namespace, so that adding a new attribute to this namespace does not need a new RFC anymore. |
I'm a big fan of this proposal. Using synchronous I/O from inside async functions is a common footman. It's worth noting that the same attribute would be useful to rayon and likely other parallel execution environments, as they too use a fixed size threadpool.
On Fri, May 17, 2024, at 2:42 PM, Esteban Kuber wrote:
`#[diagnostic::blocking]` is a marker attribute for functions that are considered (by their author) to be a blocking operation, and as such shouldn't be invoked from an `async` function. `rustc`, `clippy` and other tools can use this signal to lint against calling these functions in `async` contexts.
It would allow `rustc`, `clippy` and other tools to provide lints like
`warning: async function `foo` can block
--> $DIR/blocking-calls-in-async.rs:28:1
|
28 | async fn foo() {
| --------------
29 | interesting();
| ^^^^^^^^^^^^^`foo` is determined to block because it calls into `interesting`
|
note: `interesting` is considered to be blocking because it was explicitly marked as such
--> $DIR/blocking-calls-in-async.rs:5:1
|
5 | #[diagnostic::blocking]
| ^^^^^^^^^^^^^^^^^^^^^^^
6 | fn interesting() {}
| ----------------
`
… Rendered <https://github.com/estebank/rfcs/blob/diagnostic-blocking/text/0000-diagnostic-blocking.md>
You can view, comment on, or merge this pull request online at:
#3639
Commit Summary
• a3d24bb <a3d24bb> Add `#[diagnostic::blocking]` attribute
File Changes
(1 file <https://github.com/rust-lang/rfcs/pull/3639/files>)
• *A* text/0000-diagnostic-blocking.md <https://github.com/rust-lang/rfcs/pull/3639/files#diff-07c20d9f37ddd58360b6b9ed610fea949cb1ffb81093f7363f9a4dbe3c4982c3> (82)
Patch Links:
• https://github.com/rust-lang/rfcs/pull/3639.patch
• https://github.com/rust-lang/rfcs/pull/3639.diff
—
Reply to this email directly, view it on GitHub <#3639>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AABF4ZWF5OZ2KTYKBOOV27LZCZFP7AVCNFSM6AAAAABH4TSV7OVHI2DSMVQWIX3LMV43ASLTON2WKOZSGMYDGMZWHEYDGMA>.
You are receiving this because you are subscribed to this thread.Message ID: ***@***.***>
|
We could be more explicit with the definition of blocking.
Note that CPU-bound operations and memory access cannot be warned using this lint. |
Using |
I don't think you'll run the poll loop directly inside |
I like this idea, and I think it's valuable to add it even if it's used just for However, there's a big gray area of what is blocking and isn't: https://blog.yoshuawuyts.com/what-is-blocking/ Maybe this diagnostic could support specifying a level of "severity", which could be configurable in
|
File IO is definitely the category of functions that needs the most programmer education of "yes, this is actually blocking", due to the prevalence of both networked file systems and disk hardware errors leading to unbounded waits. |
That's quite generational, depending on whether one has exp [seek head noises] erienced what came before SSDs. |
I expect some uses of pub struct AtomicId(Mutex<[u8; 32]>);
impl AtomicId {
pub const fn new(v: [u8; 32]) -> Self {
Self(Mutex::new(v))
}
pub fn load(&self) -> [u8; 32] {
*self.0.lock().unwrap()
}
pub fn store(&self, v: [u8; 32]) {
*self.0.lock().unwrap() = v;
}
} |
Can we also put this information into |
In the way that I'm doing analysis today, that use would indeed be caught (I'm doing transitive analysis), but in my case I have a complementary annotation to mark an item as "assume non blocking" and in this proposal the only checks being made would be for direct usages, so your wrapper would fly under the radar (regardless of whether it is really blocking or not). The correct analysis for |
That's exactly what |
I'd love to have this annotation and any sort of analysis for it.
IMO, the best approach to this problem is that Similarly, filesystem and stdio operations must in general be considered blocking, but a specific application might hold the position “I don't care about the near-zero amount of blocking that will happen due to these very occasional bits of text I write to stderr under normal circumstances”. (All of this is out of scope of the RFC — unless it somehow leads to a conclusion that blocking is too hopelessly ambiguous to introduce the attribute at all.)
That would not catch all cases. If the mutex might be held by non-async code for a long time, then async code calling |
- What parts of the design do you expect to resolve through the implementation of this feature before stabilization? | ||
- What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC? | ||
|
||
# Future possibilities |
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.
Another future possibility that I think is worth mentioning: adding syntax to the attribute to (extensibly) classify the kind or degree of blocking, so as to be able to resolve “is this really a blocking operation” edge cases by writing down “it is if you call this blocking” in some fashion.
#[diagnostic::blocking(acquires_lock)]
#[diagnostic::blocking(filesystem)]
#[diagnostic::blocking(stderr)]
#[diagnostic::blocking(::rayon::block_on_task)]
A note on that last one: if you're using Rayon, you care a lot about the difference between “this might block on the completion of another task in the Rayon thread pool” (great when inside the pool due to being able to yield to other tasks using the Rayon-specific mechanism, but bad from an async function) and “this might block acquiring a lock” (very dangerous to do from in the pool, because it can lead to deadlocks, as well as more ordinary CPU starvation due to blocking inside a thread-per-core pool).
Much like async, if you're not already attuned to this class of issue, it can be easy to make a mistake by accident, and it's hard to audit for, so being able to use the same kind of “don't call X from Y because you could deadlock/starve” analysis but with a narrower classification would be quite useful.
one place where blocking should be allowed is in things like genawaiter, which is a library for making generators built on top of the async/await mechanism, where it's like having an iterator that may block, which is fine. |
To me uncontended Mutex isn't "blocking". There are many usage patterns where the critical section is small and quick. |
The And given that |
I'm saying there should be an attribute like #[nonrecursive_allow(blocking)]
async {
blocking(); // won't warn
foo(async {
blocking(); // warns
});
} |
@kornelski technically an uncontended mutex should be |
Nah, In practice, blocking is only an issue when |
To add some further fuel: I am very much against Mutex::lock being 'blocking'. A mutex with a very short critical section will always be faster than an async lock like tokio's because tokio uses a std::sync::Mutex for the wait list: If tokio considers a mutex to be fast enough, then so should we. It would be very annoying to have to annotate the many such uses in my codebases. That being said, I do respect that many use cases of mutex in async code has the potential to be slow, and I am supportive of static analysis and lints to try and improve the status quo. However, even a hashmap realloc I have measured to be several ms. An async lock won't fix that if you do Onto a slightly related but different topic, I'd like to see some investment into tools for debugging actual blocking in code. For instance, a watcher thread that monitors runtime threads, periodically polling them and sampling stack traces. If it detects that a thread has taken a while to yield, it can dump the stacktrace so you can debug uses of |
what about we split them to different types of blocking functions ( |
Those categories do not carve reality along its joints. File IO can be faster or slower than network IO. Mutexes depends on the critical section, contention, potential for priority inversion, ... If you want to know if library calls are fast and compose well, then you need them to be
Then you can add up all the constant latencies and see if they are below your responsiveness goal for a feature of your application. If things are not constant-time then you can quickly get O(n²) behavior due to loops. If you do not know the expected latency of calls then all that nice constant-timeness is useless if the constants are huge. If they aren't wait-free then you run into starvation situations based on system state. Making low-latency code legible to the compiler is hard. Perhaps there's some prior art in hard realtime programming that we can look at? |
For me personally, I'd prefer if mutexes were listed as blocking. If a mutex guards a short enough critical section, so then (using the lint reasons feature that's about to become stable) there will be a place where I can document that assumption in the code: async fn something_with_mutex() {
static SHARED_MUTEX: Mutex<_> = ..;
#[allow(
blocking_in_async,
reason = "This mutex only guards `do_something` calls, which finish quickly enough."
)]
SHARED_MUTEX.lock().do_something();
} To me, a good part of the Rust language is the many ways in which it forces me to do my homework and show that what I'm doing is correct, and this feels like another way to do that. |
i feel like once your latency requirements become strict enough the only solution is nonlocal control flow (signals, interrupts, or preemptive multitasking) and to add some fuel to the "what does blocking even mean" fire, is i think trying to estimate performance using static analysis is outside the scope of the compiler, and would be better suited with dynamic analysis (measuring time between waits) is there even a way to manually yield an async fn when doing long cpu bound processing like keygen? it seems like you would want to await a future that returns "Pending" exactly once. |
Yes. Tokio offers a yield_now function which is very easy to remake. It calls wake immediately and returns pending once. I have previously used that during password hashing, for instance: https://github.com/neondatabase/neon/blob/a5ecca976ec3abf97c8c3db4f78c230b4553b509/proxy/src/scram/exchange.rs#L98 Alternatively tokio has a concept of a "cooperative budget", which is not yet stable api tokio-rs/tokio#6622, but it will only return pending if the budget is fully consumed. Budget is acquired by most tokio functions like reading data from a TcpStream or sending into a channel (both of these otherwise could always be immediately ready in a loop and never yield) |
The final version would be to implement a call-graph analyisis lint so that transitive calls to blockign operations also produce a warning, but this is explicitly out of scope of this RFC. | ||
|
||
# Drawbacks | ||
[drawbacks]: #drawbacks |
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.
Another potential drawback is that, um, "enthusiastic" contributors might make unwelcome PRs against a large number of crates adding this annotation to basically every method. Thoughtful documentation/messaging around the feature might be able to mitigate that risk
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.
Any thoughts on a warning for unnecessarily applying #[diagnostic::blocking]
on functions that can already be inferred to be blocking? That could help reduce some noise as well.
Prior discussion from IRLO based on runtime detection and panicking. |
#[diagnostic::blocking]
is a marker attribute for functions that are considered (by their author) to be a blocking operation, and as such shouldn't be invoked from anasync
function.rustc
,clippy
and other tools can use this signal to lint against calling these functions inasync
contexts.It would allow
rustc
,clippy
and other tools to provide lints likeRendered