-
Notifications
You must be signed in to change notification settings - Fork 101
[RFC] Add a Mutex trait #377
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
Conversation
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.
Writing from mobile, having limited means to look stuff up, so please correct if wrong.
IIRC, std::mutex lock() creates a RAII guard that one can dereference to get access to the data.
This trait's lock function expects a closure. I personally tend to like closures more because they give a better visual indication of the critical section, but wanted to ask nonetheless if a guard approach was considered to stay closer to std::?
The guard impl could be done by us, trait would have to come from the user.
I prefer a closure-based locking solution. There are certain kinds of locks that are not implementable by a guard because the destructor can be moved around and reordered. For instance, locks that backup and disable the current status of interrupts cannot (easily) be reordered. It's possible to implement a closure-based lock on top of a guard lock, but not the other way around. E.G. fn lock(&self, closure: F) {
let guard = self.inner_lock.lock();
closure(&mut *guard);
} As such, I think a closure-based design is better. |
You mean the compiler moving around the destructor? |
From my side there are 4 reasons why this should not be based on RAII, but a closure:
Lets continue discussing, I'll update the RFC with these thoughts after. :) |
@andre-richter I mean you can move around the Guard. E.G. let x = lock1.lock();
let y = lock2.lock();
drop(x);
drop(y); Here, lock1 is locked before lock2, but unlocked first. This might work for some types of mutexes, but others will be hard (or impossible) to implement this way - such as locks that simply backup and disable interrupts on single-core systems. Closures ensure a hierarchy in the order in which mutexes are locked/unlocked. |
Yeah, like I said, I'm in favor of closures as well. Thanks for providing the feedback so far. I just wanted to point out that users coming from standard world might be confused for a second but it's a price we can easily pay. In the past I wanted to check/benchmark if RAII vs. closure has any (minimal) performance differences. If somebody feels like writing a micro benchmark, this would be a nice opportunity now and potential content for the (dis)advantages section. |
After inlining the performance should be the same as they fundamentally do the same operations, but if someone wants to benchmark I can add the results. Another thing that came up with RAII based Mutex, if someone runs |
Yeah I was actually wondering if there are situations where the compiler would decide to call the closure as a function. Agree that with inlining it is zero overhead. We could put a note about what is needed (if any) to guarantee that inlining is done, or what should be avoided in a release build b/c it would defeat inlining. |
I've found https://github.com/japaric/cortex-m-rtfm/blob/2596ea0e46bec73d090d9e51d41e6c2f481b8e15/book/en/src/internals/critical-sections.md which explains in much finer details the reasoning behind |
Thanks, I will add it for clarification |
Follow up points from discussion on Matrix:
|
It think it should be an
There's also precedence in the
|
There's a difference though. This trait is unsafe because other code is allowed to assume the returned value is on a valid utf8 boundary. If an implementation fails to respect this, UB and memory corruption are likely to occur. This requirement is not actually encoded in the type system, so In our case, the requirement is that the closure is "guaranteed exclusive access to the data contained within the mutex for the duration of the lock." But this is already guaranteed by the type system: We give the closure an |
@roblabla , I'm not sure I understand. To implement the Mutex trait, should be unsafe (we have to take responsibility that the /Per |
@perlindgren All of the requirements for safe usage of this trait are encoded by the type system. Safe usage of this trait requires that the closure has exclusive access to T. The closure gets a &mut T. An If RTFM has an example of a completely safe implementation of Mutex called Exclusive: https://github.com/japaric/cortex-m-rtfm/blob/master/src/lib.rs#L311. It's trivial to know this implementation of Mutex is correct because the invariants that the trait must guarantee are encoded in the type system. |
Sorry for replying this late ...
Yes, the idea you outlined in the code is what I had thought about as well. Though this only works for a Another thing I'd like to talk about is the RTFM example you provided here. From my understanding of RTFM it is not realistic to create the driver instances in the tasks as that will mean it is recreated every time the task runs. Instead we'd want an implementation where the instances are created in I think such an implementation is not possible with a design close to the current
I agree, adding the trait MutexCreate: Mutex {
fn create() -> Self;
} and would argue this should also be a part of
Oh, I had completely overlooked this! In that case I agree, the associated type makes way more sense. Would you mind detailing this design decision in the RFC?
I'd be much more comfortable with something going in this direction because the #[derive(Clone, Debug)]
struct MutexRef<M: Mutex + Clone>(M);
impl<M: Mutex + Clone> Mutex for MutexRef<M> {
type Data = M::Data;
fn lock<R>(&mut self, f: impl FnOnce(&mut Self::Data) -> R) -> R {
self.0.lock(f)
}
}
impl<M: Mutex + Clone + MutexCreate> MutexCreate for MutexRef<M> {
fn create() -> Self {
MutexRef(M::create())
}
}
The Type A vs Type B stuff was meant to be about the interface the mutex provides, not its implementation. The point where the two meet is where some implementations are not compatible with some mutex APIs. Which is totally fine. I would just like to see all capabilities exposed for mutex implementations which have them. And in my opinion this needs to be part of My suggestion is this:
This would allow mapping like this:
|
Uuh, shouldn’t this take a Murex::Data? I expected something more like trait MutexCreate: Mutex {
fn create(data: Self::Data) -> Self;
} Otherwise the mutex wouldn’t get initialized. But this begs the question, why not simply use IMO MutexCreate shouldn’t exist if it’s going to be a seperate trait. It’s redundant, Rust already has a plethora of construction traits. |
I have an example that might be of interest here. However, this would not fit as well when multiple steps are necessary for an operation like in the driver for xca9548a (hardware I2C switch/multiplexer, useful for setups where several slaves share an address, for example). let parts = i2c_switch.split();
let mut my_driver = Driver::new(parts.i2c0);
let mut my_other_driver = Driver::new(parts.i2c1); When interacting with these virtual devices, before doing the actual What would be the best way to provide the user with the freedom to say whether accesses need to be mutexed or not and if so, provide the concrete type for the |
@roblabla: Wow, yes, how did I miss this?!
Makes sense. Though I think adding a trait MutexCreate: Mutex + From<Mutex::Data> { }
impl<M: Mutex + From<Mutex::Data>> MutexCreate for M { } |
@eldruin: We have the same issue in |
Hi all, I'd like to thank all for all their input into this issue! We are going to start FCP now (1 week long), with the following:
Thanks again for enlightening discussions! |
Something we have not yet talked about at all is that the current design does not provide any way to acquire a mutex in a non-blocking fashion. This makes the mutex unsuitable for use in ISRs and similar contexts which are not allowed to block under any circumstances. It would probably be a good idea to include a second lock method that immediately returns if the mutex cannot be acquired. That is: pub trait Mutex {
type Data;
fn lock<R>(&mut self, f: impl FnOnce(&mut Self::Data) -> R) -> R;
// Attempt locking the mutex but return immediately if this is not possible
fn try_lock<R>(&mut self, f: impl FnOnce(&mut Self::Data) -> R) -> nb::Result<R, ()>;
} |
@Rahix This is by design and ties back to the original fallible vs infallible discussion. An fallible mutex can be implemented on top of this, however this RFC is specifically for an infallible mutex as we discussed earlier. In the continuation of this we will look into expanding these traits and implementations and then this direction should have its own RFC/issue together with a specific discussion on only this to not miss important things as can happen if we make one RFC too large. When we have the base crate up I will make issues for each of the extra features and ideas here and link in each person connected to it so each extension can get its own discussion. |
The way I understand the fallible vs. infallible discussion, an infallible mutex is just defined to always be able to acquire the mutex eventually. There are no guarantees about when that will happen and there might very well be a considerable time between start of the locking attempt and actual acquisition. That means, there might be a time where the caller is blocked, waiting on the current mutex holder to release it. This behavior makes the As a parallel, take a look at the mutex implementation in the Linux kernel: void mutex_lock(struct mutex *lock); The But for IRQs, tasklets and other contexts where rescheduling or any other kind of blocking is not allowed, there is a second int mutex_trylock(struct mutex *lock); function that returns immediately whether it was able to acquire the mutex or not. This function is now safe for any kind of context.
I fear that standardizing a trait and expanding it later will be very painful as we saw with the |
Can you say more about the use case you have in mind for this generic I think I should say at this point -- not to anyone in particular -- that a Traits (not extension traits, though) are mainly a code reuse mechanism; they
IMO, in this case one should look for and use a particular mutex that has the To give an example, under this From this example alone, I would not add
I don't think this is a good comparison. This Linux API is a concrete function /
Very different scenario. This In conclusion, I don't think a |
Not 100% sure what @Rahix had in mind, but I can see a failable mutex being useful in two situations:
For point 1, I can see why RTFM is advantageous, and can be considered to "obsolete" these needs, but for simple use cases, it may be useful to occasionally synchronize data without caring too much if we can't always obtain the mutex. For example if there are two "tasks" which both want to access the I2C bus (one to read a temperature sensor, once a second, the other to read an O2 sensor, once every 100ms), it would be nice to be able to simply wrap this in a non-blocking mutex, and just skip update periods when necessary. This isn't suitable for hard realtime or timing critical use cases, but sometimes it's okay to skip a sensor update every now and then if the bus is busy. For the second use case (multicore), I could see a shared memory buffer that is protected by some kind of spinlock or basic CAS primitive for the purposes of a sharing data between two systems. We don't want to necessary block one core while the other is writing data, particularly if one is just seeing if any messages are pending. |
I agree with these use cases. There are certainly uses cases for In these two examples I would just go to crates.io and shop for a mutex with the right semantics not caring much about the API (the "shape"; i.e. closure API vs guard-based API). |
There is no issue in having a However every extension we propose should have its own RFC to get proper focus and discussion as this is a very fundamental core area which is very easy to get wrong if we do not thoroughly think it through. |
So I think I've been convinced that any scenario requiring a failable mutex (like round-robin processing of co-operative tasks which greedily acquire resources and hold on to them beyond their run-time as some hardware operation - e.g. DMA - is on-going in the background) can be resolved by using some other type (e.g. an atomic counter) and a infallible mutex. I agree with @japaric that |
For what it's worth I also think having a separate I don't think there's a good solution to that however, even if there was a nice standard chances are people would still only implement the minimum enforced functionality so it becomes a question of what's considered required functionality for a Mutex in general. |
Can someone please answer #377 (comment) ? |
We've had a lot of discussion, and decided in the meeting this is ready to merge. Thanks all! bors r+ |
👎 Rejected by label |
bors retry |
377: [RFC] Add a Mutex trait r=jamesmunns a=korken89 cc @perlindgren @japaric [Rendered](https://github.com/korken89/wg/blob/master/rfcs/0377-mutex-trait.md) Co-authored-by: Emil Fresk <[email protected]>
Build succeeded |
cc @perlindgren @japaric
Rendered