I started playing with rust last week (just converting a couple of C# projects so far), and I'm going to say that once you understand that mutexes/rwlocks are wrappers around the actual data, it (to me at least) feels better.
Don't get me wrong, it's an absolute headache for anyone that's acquired intermediate or better skill in one of the Cx languages. The paradigm shift is still hitting me hard. But this was one of the differences I actually think is an improvement in probably most use cases.
5C5C5C@programming.dev
on 16 Sep 2024 14:35
nextcollapse
It’s a massive win, and I would question the credibility of any systems programmer that doesn’t recognize that as soon as they understand the wrapper arrangement. I would have to assume that such people are going around making egregious errors in how they’re using mutexes in their C-like code, and are the reason Rust is such an important language to roll out everywhere.
The only time I’ve ever needed a Mutex<()> so far with Rust is when I had to interop with a C library which itself was not thread safe (unprotected use of global variables), so I needed to lock the placeholder mutex each time I called one of the C functions.
sugar_in_your_tea@sh.itjust.works
on 16 Sep 2024 22:06
nextcollapse
Exactly. If there’s only one thing I could bring from Rust into another language, it would be Mutexes. It’s so nice to guarantee safe access to data.
nous@programming.dev
on 17 Sep 2024 11:19
collapse
Rust mutexes would be nice. But I think for me that one thing for me would be its enums.
5C5C5C@programming.dev
on 18 Sep 2024 07:12
collapse
But only if pattern matching were included, otherwise they would be as unpleasant as C++'s std::variant.
SorteKanin@feddit.dk
on 17 Sep 2024 20:47
collapse
The only time I’ve ever needed a Mutex<()> so far with Rust is when I had to interop with a C library which itself was not thread safe (unprotected use of global variables), so I needed to lock the placeholder mutex each time I called one of the C functions.
Actually I think in this case you’re still better off using a Mutex with “data” inside. I’ve done this before. The idea is that you make a unit struct MyCFuncs or whatever and then you only call the C functions from methods of that unit struct. Then you can only access those methods once you lock the Mutex and get the instance of the unit struct. It feel elegant to me.
5C5C5C@programming.dev
on 18 Sep 2024 00:44
collapse
This makes a lot of sense, but the functions were Rust bindings for plain C functions, they weren’t function pointers. Granted I could have put pointers to the function bindings into fields in a struct and stored that struct in the mutex, but the ability to anyhow call the bindings would still exist.
SorteKanin@feddit.dk
on 18 Sep 2024 05:46
collapse
They were also plain C functions in my case, but it doesn’t take too much discipline to only call it through the struct. Also, you can put the struct in a different crate which includes the C bindings to ensure that you can’t call the C bindings without the struct.
Wrapping a value in a mutex just makes sense. After learning a bit of Rust I made a similar mutex wrapper in C++ when I had to protect a class member in a C++ project. I just had to change the type in the declaration, and bam the compiler tells me about all places this member was accessed. Much easier than using some buggy ‘find all references’, potentially forgetting a few places.
lolcatnip@reddthat.com
on 16 Sep 2024 15:06
collapse
Looks like the author missed my main complaint about Rust mutexes, which is that the lock method returns a Result. There should be a try_unlock method for when someone actually wants to handle the rather obscure failure case, and the name lock should be used for a method that panics on failure but returns a value that doesn’t need to be unwrapped first. I see the current arrangement as being about as sensible as having array subscripting return a Result to handle the case of a failed bounds check.
BB_C@programming.dev
on 16 Sep 2024 21:44
nextcollapse
If lock-ergonomics^ⓒ^ is as relevant to you as indexing, you’re doing it wrong.
I would rather take indexing returning Results than the other way around.
One can always wrap any code in {||{ //… }}() and use question marks liberally anyway (I call them stable try blocks 😉).
sugar_in_your_tea@sh.itjust.works
on 16 Sep 2024 22:03
nextcollapse
I kind of disagree here. .lock() has the following behavior:
panic() if the lock is already held by this thread - should never happen
error - if the current lock holder paniced
The second case is incredibly rare, so it’s one of the few cases where I think .unwrap() makes sense in production code. But it should be an option to handle it in robust code that should never go down. This is rare, but it’s not so rare that we should force all locks to exist in a context where we can recover from panics.
.try_unlock() should never exist because there should only be one way to release a lock: drop(). Having a way to maybe unlock a mutex adds a ton of issues. If we assume this was a typo, .try_lock() absolutely exists, and it’s for a non-blocking lock.
lolcatnip@reddthat.com
on 17 Sep 2024 01:00
collapse
try_lock already exists; it’s called lock. I just want a more convenient name and I want the name of the new method to be lock, but that ship has sailed.
BB_C@programming.dev
on 17 Sep 2024 06:56
collapse
if you’re really that bothered…
use std::sync::{Mutex, MutexGuard};
trait ULock<'a> {
type Guard;
fn ulock(&'a self) -> Self::Guard;
}
impl<'a, T: 'a> ULock<'a> for Mutex<T> {
type Guard = MutexGuard<'a, T>;
fn ulock(&'a self) -> Self::Guard {
self.lock().unwrap()
}
}
or use a wrapper struct, if you really really want the method to be called exactly lock.
lolcatnip@reddthat.com
on 17 Sep 2024 17:51
collapse
I think a better solution would be to add a method called something like ulock that does a combined lock and unwrap.
My concern with lock+unwrap is only partly because of convenience; I also didn’t like it because I think it’s a bad idea to get people used to casually calling unwrap, because it tends to hide inadequate error handing.
Now that I think about it, I don’t like how unwrap can signal either “I know this can’t fail”, “the possible error states are too rare to care about” or “I can’t be bothered with real error handing right now”. In one or two of those cases you want to leave it in my production code, and in the last you want to audit all instances and replace them with proper error handing. Using the same function for all three cases makes that difficult.
BB_C@programming.dev
on 17 Sep 2024 19:57
nextcollapse
a better solution would be to add a method called something like ulock that does a combined lock and unwrap.
That’s exactly what’s done above using an extension trait! You can mutex_val.ulock() with it!
Now that I think about it, I don’t like how unwrap can signal either “I know this can’t fail”, “the possible error states are too rare to care about” or “I can’t be bothered with real error handing right now”.
That’s why you’re told (clippy does that i think) to use expect instead, so you can signal “whatever string” you want to signal precisely.
sugar_in_your_tea@sh.itjust.works
on 18 Sep 2024 04:05
collapse
Exactly! My code has a handful of “expect()” calls in it, and each one self-documents why it’s okay. It’s like a comment, but it appears in logs if it ever triggers.
5C5C5C@programming.dev
on 18 Sep 2024 07:19
collapse
Best practice when using .unwrap() in production code is to put a line of documentation immediately above the use of .unwrap() that describes the safety invariants which allow the unwrap to be safe.
Since code churn could eventually cause those safety invariants to be violated, I think it’s not a bad thing for a blunt audit of .unwrap() to bring your attention to those cases and prompt to reevaluate if the invariants are still satisfied.
SorteKanin@feddit.dk
on 17 Sep 2024 20:50
collapse
threaded - newest
I started playing with rust last week (just converting a couple of C# projects so far), and I'm going to say that once you understand that mutexes/rwlocks are wrappers around the actual data, it (to me at least) feels better.
Don't get me wrong, it's an absolute headache for anyone that's acquired intermediate or better skill in one of the Cx languages. The paradigm shift is still hitting me hard. But this was one of the differences I actually think is an improvement in probably most use cases.
It’s a massive win, and I would question the credibility of any systems programmer that doesn’t recognize that as soon as they understand the wrapper arrangement. I would have to assume that such people are going around making egregious errors in how they’re using mutexes in their C-like code, and are the reason Rust is such an important language to roll out everywhere.
The only time I’ve ever needed a
Mutex<()>
so far with Rust is when I had to interop with a C library which itself was not thread safe (unprotected use of global variables), so I needed to lock the placeholder mutex each time I called one of the C functions.Exactly. If there’s only one thing I could bring from Rust into another language, it would be Mutexes. It’s so nice to guarantee safe access to data.
Rust mutexes would be nice. But I think for me that one thing for me would be its enums.
But only if pattern matching were included, otherwise they would be as unpleasant as C++'s
std::variant
.Actually I think in this case you’re still better off using a Mutex with “data” inside. I’ve done this before. The idea is that you make a unit struct
MyCFuncs
or whatever and then you only call the C functions from methods of that unit struct. Then you can only access those methods once you lock the Mutex and get the instance of the unit struct. It feel elegant to me.This makes a lot of sense, but the functions were Rust bindings for plain C functions, they weren’t function pointers. Granted I could have put pointers to the function bindings into fields in a struct and stored that struct in the mutex, but the ability to anyhow call the bindings would still exist.
They were also plain C functions in my case, but it doesn’t take too much discipline to only call it through the struct. Also, you can put the struct in a different crate which includes the C bindings to ensure that you can’t call the C bindings without the struct.
Wrapping a value in a mutex just makes sense. After learning a bit of Rust I made a similar mutex wrapper in C++ when I had to protect a class member in a C++ project. I just had to change the type in the declaration, and bam the compiler tells me about all places this member was accessed. Much easier than using some buggy ‘find all references’, potentially forgetting a few places.
Looks like the author missed my main complaint about Rust mutexes, which is that the
lock
method returns aResult
. There should be atry_unlock
method for when someone actually wants to handle the rather obscure failure case, and the namelock
should be used for a method that panics on failure but returns a value that doesn’t need to be unwrapped first. I see the current arrangement as being about as sensible as having array subscripting return aResult
to handle the case of a failed bounds check.If lock-ergonomics^ⓒ^ is as relevant to you as indexing, you’re doing it wrong.
I would rather take indexing returning
Result
s than the other way around.One can always wrap any code in
{||{ //… }}()
and use question marks liberally anyway (I call them stable try blocks 😉).I kind of disagree here.
.lock()
has the following behavior:panic()
if the lock is already held by this thread - should never happenThe second case is incredibly rare, so it’s one of the few cases where I think
.unwrap()
makes sense in production code. But it should be an option to handle it in robust code that should never go down. This is rare, but it’s not so rare that we should force all locks to exist in a context where we can recover from panics..try_unlock()
should never exist because there should only be one way to release a lock:drop()
. Having a way to maybe unlock a mutex adds a ton of issues. If we assume this was a typo,.try_lock()
absolutely exists, and it’s for a non-blocking lock.try_lock
already exists; it’s calledlock
. I just want a more convenient name and I want the name of the new method to belock
, but that ship has sailed.if you’re really that bothered…
or use a wrapper struct, if you really really want the method to be called exactly
lock
.I think a better solution would be to add a method called something like ulock that does a combined lock and unwrap.
My concern with lock+unwrap is only partly because of convenience; I also didn’t like it because I think it’s a bad idea to get people used to casually calling unwrap, because it tends to hide inadequate error handing.
Now that I think about it, I don’t like how unwrap can signal either “I know this can’t fail”, “the possible error states are too rare to care about” or “I can’t be bothered with real error handing right now”. In one or two of those cases you want to leave it in my production code, and in the last you want to audit all instances and replace them with proper error handing. Using the same function for all three cases makes that difficult.
That’s exactly what’s done above using an extension trait! You can
mutex_val.ulock()
with it!That’s why you’re told (clippy does that i think) to use
expect
instead, so you can signal “whatever string” you want to signal precisely.Exactly! My code has a handful of “expect()” calls in it, and each one self-documents why it’s okay. It’s like a comment, but it appears in logs if it ever triggers.
Best practice when using
.unwrap()
in production code is to put a line of documentation immediately above the use of.unwrap()
that describes the safety invariants which allow the unwrap to be safe.Since code churn could eventually cause those safety invariants to be violated, I think it’s not a bad thing for a blunt audit of
.unwrap()
to bring your attention to those cases and prompt to reevaluate if the invariants are still satisfied.Just use the Mutex from the parking_lot crate.