Skip to content

Q: help understanding NoopRawMutex vs. ThreadModeRawMutex #4034

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

Open
akavel opened this issue Apr 1, 2025 · 2 comments
Open

Q: help understanding NoopRawMutex vs. ThreadModeRawMutex #4034

akavel opened this issue Apr 1, 2025 · 2 comments

Comments

@akavel
Copy link

akavel commented Apr 1, 2025

I am starting to try learning and using async features in embassy, and preparing to share data between tasks. Through the examples I found at: https://embassy.dev/book/#_sharing_peripherals_between_tasks, I understand, that when instantiating various syncing types (like Mutex, channels, etc.), I will need to make a choice of a "raw" mutex to specialize them with. In the embassy_sync::blocking_mutex::raw namespace, I see three types. The CriticalSectionRawMutex seems only necessary when sharing data between interrupts and non-interrupt tasks, this sounds clear from its description.

However, I'm super confused as to when I should use the NoopRawMutex vs. the ThreadModeRawMutex. Based on the description in async Mutex, they both seem explained as for use when data is "shared between tasks running on the same executor". Ok, so I cannot use either when sharing between tasks on different executors, got it. But then, the ThreadMode... one seems to get some additional qualifiers - in one place, it is described as "...but you want a singleton", and in another one as "...only allows borrowing from thread mode". Both of those qualifiers don't seem to tell me much that I would manage to understand:

  • as for "you want a singleton": isn't the whole point of mutexes in general to guard a "singleton" resource? I don't seem to be able to envision an alternative at the moment; does it mean that the NoopRawMutex results in the guarded object being copied? how is it a mutex in such case? Or does this mean something completely else that I don't get?
  • as for "borrowing from thread mode": what is a "thread mode"? I don't seem to recall seeing this mentioned elsewhere in embassy docs; I only remember seeing "tasks" mentioned - what's their relation to "threads"? or is it some completely different concept? and again - how does this differ to NoopRawMutex? isn't the whole point of using any kind of mutex to share data between tasks? again, I'm finding myself making a blank stare with a feeling of not knowing what to grasp to get some firm ground...

I'd be super grateful for some help trying to understand those! I tried googling up, I tried searching the issues, checking the FAQ, still haven't managed to find any other explanation...

edit: to add to my confusion, from what I recall some examples seemed to use the NoopRawMutex, while others the ThreadModeRawMutex - but I still wasn't able to decipher the criteria for when I should choose one over the other...

TIA!

@jamesmunns
Copy link
Contributor

Basically:

ThreadModeRawMutex impls Sync, and NoopRawMutex doesn't.

This means that ThreadModeRawMutex can be put in a static, or StaticCell, allowing resources to be shared with any code that is in "Thread Mode" (non-interrupt context) on a single-core chip. If it is accessed by an interrupt, it will panic.

Conversely, NoopRawMutex cannot be put in a static, and since you can't pass borrowed data to other tasks, it can't be used to share data across tasks. You can use NoopRawMutex locally within a task, shared across multiple futures potentially.

The use case for NoopRawMutex would be if you are using something like a shared i2c/spi port within a single task, and you need a mutex for guarding access to the bus.

The "Raw" part of the mutex is what guards the Mutex's state itself:

  • Checking if the Mutex is locked (yielding if so, continuing if not)
  • Marking the Mutex as locked when gaining access
  • Marking the Mutex as unlocked when the guard is dropped

With ANY Raw mutex, the outer Mutex acts the same: If the resource is locked, we yield and await. If the resource is not, we take exclusive access and continue. The Raw Mutex guarantees only take place at lock/unlock, NOT the entire time the outer Mutex is locked.

In general:

  • CriticalSectionRawMutex needs to take a critical section while checking if the Mutex is already "locked" or not, so it has the highest "cost"
  • ThreadModeRawMutex just needs to check "are we in interrupt context" while checking if the Mutex is already "locked" or not (using the VECTACTIVE register), so it's cheaper than a critical section
  • NoopRawMutex knows that we are in a single core, non-interupt context, so it can freely check if the Mutex is already "locked" or not by just checking a boolean in the mutex, so it is the cheapest (only by an extremely little bit under ThreadModeRawMutex, mostly because there's no potential panicking branch)

In general:

  • If you want to put your Mutex in a static, AND an interrupt (or async code in an Interrupt Executor) might touch it: Use a CriticalSectionRawMutex.
  • If you want to put your Mutex in a static, ONLY with non-interrupts/InterruptExecutor tasks: Use a ThreadModeRawMutex.
  • If you ONLY use your Mutex locally, and never put it in a static, Use a NoopRawMutex.

@akavel
Copy link
Author

akavel commented Apr 9, 2025

Ahhhh, that's an awesome explanation, thank you so, so much, for caring and for your time and effort writing it up! 😍 Honestly, I'd love if you would just copy & paste it verbatim into the rustdoc for future readers 😃 (including a future me 😂)

To summarize in my words - please correct me if I got it wrong! - the difference between ThreadModeRawMutex and NoopRawMutex is thus:

  • ThreadModeRawMutex - can be used both in #[task] functions (thanks to Sync), as well as in futures - just it's a bit of a waste to use it in futures without tasks; (how about then renaming/aliasing it as TaskModeRawMutex, for unified naming? or, is "thread mode" mentioned somewhere in tasks docs? or, maybe at least just explaining that "Thread Mode" means #[tasks] in the docs?) - but they cannot be shared with interrupts, which Rust can't protect against statically, only at runtime via a panic;
  • NoopRawMutex - Rust typesystem won't allow to share it between #[task] functions (because it has no Sync), so it will only work shared between futures in a single #[task] - but is the cheapest one and will work totally fine in & between those futures.

Did I get it reasonably right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants