-
-
Notifications
You must be signed in to change notification settings - Fork 21
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
feat(maitake-sync): mutex-traits
integration
#482
Conversation
@jamesmunns this is just about done, i basically just need to finish docs. |
Do you know what's up with the loom test failures? Is that just needing a rename? |
Yeah, I haven't actually updated the test-only code yet, I'll need to do that as well. |
maitake-sync/src/wait_queue.rs
Outdated
@@ -175,7 +177,7 @@ mod tests; | |||
/// [mutex]: crate::Mutex | |||
/// [2]: https://www.1024cores.net/home/lock-free-algorithms/queues/intrusive-mpsc-node-based-queue | |||
#[derive(Debug)] | |||
pub struct WaitQueue { | |||
pub struct WaitQueue<Lock: ScopedRawMutex = Spinlock> { |
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.
Misc thought: would it be possible to use a type alias and some feature detection to pick different default locks?
The reason I ask: on embedded, if someone doesn't KNOW to switch out the spinlock, they have a pretty hidden footgun: It will work unless they share stuff with interrupts AND have an interrupt occur while the lock is held.
I can see this being a MAJOR heisenbug for someone to run in to. My suggestion would be to default to the Critical Section mutex (this might be made tricky because it's a feature!) on target-os = "none"
platforms, and a spinlock on others. Or maybe even not pick a default on those platforms? (this might mean duplicating the struct defn)
I don't know how to make the UX of this not suck, but "it works 99.9% of the time until it doesn't" for embedded platforms is rough.
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.
@jamesmunns I have conflicted feelings about this. There are some competing goals where, unfortunately, I think we have to make some tradeoffs...
In particular, I have a moderate preference for maitake-sync
not containing or depending on platform-specific code. My goal thus far is that maitake-sync
just be "plain old Rust" code and any platform-specific behaviors would be provided by the top-level application.
Furthermore, I have a strong preference that maitake-sync
not do its own build-time platform detection. In my experience, this is tricky to get right, and I don't want to be responsible for maintaining it, especially when more robust and more maintained implementations exist elsewhere in the ecosystem. Additionally, there's the issue that sometimes it's not possible to detect a platform without additional user input --- for instance, there are single-core Cortex-M parts, and multi-core Cortex-M parts, and (to my knowledge), Rust can't determine which it's targeting based on just the target spec: instead, the user must set some additional RUSTFLAGS
cfg or environment variable to indicate it. From what I've seen, there are a bunch of crates that all provide their own mechanism for indicating stuff like this, so users sometimes have to set a bunch of different env vars to indicate to various parts of their dependency graph that they are targeting a single-core platform. I'd really rather not add another flag you have to set.
Therefore, I'd much prefer the top-level application build to depend on the crate that does platform detection, and pass its types into maitake-sync
. Or, if that's unworkable, I'd rather at least have maitake-sync
depend on some other crate that does the platform detection.
Regarding critical-section
support by default, hmm. I'm open to that, but I would really prefer not to make the default mutex type parameter for everything be the mutex::raw_impls::cs::CriticalSectionRawMutex
type, since it only implements ScopedRawMutex
, meaning that by default, the RAII mutex APIs won't be available, which seems sad.
An alternative option could be to bake the critical_section::acquire
and critical_section::release
functions into maitake_sync::blocking::Mutex
itself, rather than the RawMutex
type. That way, we can always ensure that IRQ disabling and enabling behaviors happen. But, this actually causes a problem when the critical section impl isn't just IRQ-disable/IRQ-restore, but also implements a real mutex, because now you have two mutices. I guess critical-section
does require reentrancy, so it's not a deadlock, it just means you're doing extra stuff... It also kind of makes the critical-section
impl in mutex
unnecessary with maitake-sync
, which is not the end of the world, but we might have to document that. I'd be open to consider this approach, as long as the critical-section
dep is feature-flagged.
What do you think?
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.
For the record, I definitely would prefer to avoid footgun-y default behavior. However, I also kind of feel like platform-specific behaviors like this are usually also application-specific behavior, and it's hard to implement any default that really does what you want without some additional input from the end user. I dunno.
Another concern that I forgot to mention above is that I wanted to see if I could add mutex-traits
support without a breaking change --- which means that the default new
constructors need to behave identically to how they did before. They can't depend on critical-section
in that case, since IIUC critical-section
will fail to link if the user doesn't depend some critical section implementation. So, using critical-section
by default would be a breaking change, and not having default parameters would also be a breaking change.
With that said, I'm open to releasing a v0.2 if it's necessary. I just wanted to see if I could get away with not doing that. This is not nearly as strong a preference as my desire to avoid having to maintain my own platform detection code!
mutex-traits
integrationmutex-traits
integration
BTW @jamesmunns 146be31 adds a huge pile of documentation, which I would love your take on if you have time. |
@hawkw I reviewed 146be31 and it is very clear and well written! I need to look at the total diff - but IMO we should have "chef's recommendations" for the mutexes you should use, or point to These docs cover:
But we might want to answer the question "which would should I use (in the most common use cases)". |
Yeah, I totally agree with this — but, ideally, I'd like to add that in the Honesty, some of these docs should maybe be copy-pasted into |
Also, since I think we've agreed that |
@jamesmunns when you have a chance, I'd love your take on the current |
/// ensures that bare-metal users who have enabled `critical-section` get a | ||
/// mutex that disables IRQs when locked. | ||
/// | ||
/// - **Otherwise, the `DefaultMutex` is a [`Spinlock`]**. This is the default |
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.
I wonder if we could poison this with a !Sync
bound in this case? That would be enough to inhibit IRQ usage, but would also probably ruin your inter-core use case :/
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 whole point of a spinlock is to make things Sync
...if that wasn't the goal, we could just make it panic. I wondered about possibly having some kind of IrqSafe
marker trait, analogously to Send
and Sync
, but...there's no way to actually enforce it...
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.
right. whoops.
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.
I think the problem is that, although ISRs are "just another kind of concurrency" and can race in all the same ways as threads/multicore code, the things you have to do to prevent races are different.
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 is all super good, and I am very excited for it!
@jamesmunns by the way, regarding constructors, do you have a strong opinion on |
No strong opinions! I think if we have some cross linking between the constructors, it's all okay. Weakly, I prefer |
Yeah, that's a big part of why I was considering switching to |
Another thing I'm trying to make up my mind on is whether the Currently, let mutex = Arc::new(Mutex::new(String::new())); gets you an error like:
Which is pretty unfortunate. On the other hand, a struct Foo {
t: Mutex<String>,
} I am implicitly providing a type for Basically what I'm trying to decide is whether The alternative might be to just remove the alias. |
maybe don't have a I don't have strong preferences either! I personally avoid default types (and default lifetimes!), because I find they can be confusing for people less familiar with the API surface. It's less clean, but maybe more "honest"? |
As mentioned in [this comment][1], `new_with_raw_mutex` is probably a better name for the constructor, because we also have `with_lock`, which isn't a constructor. Also, it has the side benefit of being autocomplete-friendly; it will be suggested if you type `new`, which seems good. [1]: #482 (comment)
Hmm, I think that for all the async types, a But, maybe what I should do is either remove the |
Still needs docs, and plumbing through all the async primitives...
maybe `new` should infer the lock type idk...
Co-authored-by: James Munns <james@onevariable.com>
As mentioned in [this comment][1], `new_with_raw_mutex` is probably a better name for the constructor, because we also have `with_lock`, which isn't a constructor. Also, it has the side benefit of being autocomplete-friendly; it will be suggested if you type `new`, which seems good. [1]: #482 (comment)
Loom depends on `tracing` anyway, so `tracing` is always in the dependency tree when `cfg(loom)` is set. So, there isn't really any sense in disabling `tracing` in `maitake-sync` when `cfg(loom)` is set, even if the tracing feature isn't explicitly enabled.
Currently,
maitake-sync
's blockingMutex
andRwLock
use spinlocksto wait for the lock to become available. This works on (almost) all
platforms,1 but there are a number of reasons it might be less than
desirable. For example:
can also be shared with interrupt handlers. In that case, the mutex
must also disable IRQs when locked, and re-enable them when unlocked.
hardware lock elision, an OS mutex or something when not on
#![no_std]
, or an existing library RTOS mutex or similar."waiting" is inherently a deadlock, and you would prefer to make any
attempt to lock an already-locked mutex into a panic instead.
Therefore, @jamesmunns and I have written the
mutex-traits
crate, toabstract over mutex implementations. This trait is similar to
lock_api
, but with aScopedRawMutex
trait to support mutexen basedon
critical-section
, which cannot be locked and unlocked atarbitrary times and instead requires a closure.
This branch updates
maitake-sync
to usemutex-traits
. Inparticular:
Mutex
type is generic over aLock
type parameter,which may implement
mutex_traits::RawMutex
ormutex_traits::ScopedRawMutex
.Mutex
has scoped lock methods whenLock: ScopedRawMutex
, and guardmethods require
Lock: RawMutex
.Mutex
are now generic overa
Lock
type for the underlying mutex. Everything except forSemaphore
andRwLock
only requiresScopedRawMutex
, butSemaphore
andRwLock
requireRawMutex
.Mutex
es are constructed with aDefaultMutex
as theScopedRawMutex
impl, which tries to pick the right behavior based onfeatures/target config. This only implements
ScopedRawMutex
.BREAKING CHANGE:
Renamed
spin::Mutex
andspin::RwLock
toblocking::Mutex
andblocking::RwLock
.blocking::Mutex::new
now constructs ablocking::Mutex<T, DefaultMutex>
,which only provides scoped lock methods. To construct a
mutex that also has RAII guard lock methods, use
blocking::Mutex::new_with_raw_mutex
to provide aRawMutex
implementation.
Footnotes
Provided they have atomic CAS... ↩