5 ms·
I found that a lot of the problems I had been having with mutexes, stem from the fact that traditionally the mutex and the data it protects are separate. Boltin
by juliangmp 11mo ago
I found that a lot of the problems I had been having with mutexes, stem from the fact that traditionally the mutex and the data it protects are separate.
Bolting them together, like Rust's Mutex<T> does, solves a lot these problems. It let's you write normal, synchronous code and leave the locking up to the caller, but without making it a nightmare. You can't even access the data without locking the mutex.
This isn't an attack on the (very well written) article though. Just wanted to add my two cents.
- mgaunard 11mo agoI find it better to model that as an Actor than a mutex, but I guess it's inherently the same thing, except the actor also allows asynchronous operations.
- gpderetta 11mo agoYou can go full circle and also make operations on a mutex asynchronous. Hence the realization that message passing and shared memory are truly dual.
- mgaunard 11mo agoThe very idea of a mutex is that it is synchronous. You wait until you can acquire the mutex. If it's asynchronous, it's not a mutex anymore, or it's just used to synchronously setup some other asynchronous mechanism.
- gpderetta 11mo agoA mutex is a way to guarantee mutual exclusion nothing more nothing less; You can recover synchronous behaviour if you really want: synchronized<Something> something; ... co_await something.async_visit([&](Something& x) { /* critical section here */ });
- mgaunard 11mo agothat isn't a mutex, that's delegating work asynchronously and delegating something else to run when it is complete (the implicitly defined continuation through coroutines). In systems programming parlance, a mutex is a resource which can be acquired and released, acquired exactly once, and blocks on acquire if already acquired.
- gpderetta 11mo agoDo a CPS transform of your typical std::mutex critical section and you'll find they are exactly the same.
- mgaunard 11mo agoThey're not, the interactions with the memory model are different, as are the guarantees. CPS shouldn't be able to deadlock for example?
- gpderetta 11mo agoCPS can trivially deadlock for all meaningful definitions of deadlock. Would you consider this a mutex? async_mutex mux; co_await mux.lock(); /* critical section */ co_await mux.unlock(); What about: my_mutex mux; { std::lock_guard _{mux}; /* critical section */ } where the code runs in a user space fiber. Would you consider boost synchronized a mutex? Don't confuse the semantics with the implementation details (yes async/await leaks implementation details).
- mgaunard 11mo agoYou only achieved a deadlock by re-introducing mutexes.
- gpderetta 11mo agoGiven: Something someting; async_mutex mtx; void my_critical_section(Data&); 1: await mtx.lock(); my_critical_section(something); await mtx.unlock(); 2: auto my_locked_critical_section() { await mtx.lock(); my_critical_section(something); await mtx.unlock(); } ... await my_locked_critical_section(something); 3: auto locked(auto mtx, auto critical_section) { await mtx.lock(); critical_section(); await mtx.unlock(); } ... await locked(mtx, [&]{ my_critical_section(something); }); 4: template<class T> struct synchronized { async_mutex mtx; T data; auto async_visit(auto fn) { locked(mtx, [fn,&data]{ fn(data); }); } }; synchronized<Something> something; await something.async_visit([](Something& data) { my_critical_section(something); }); If 1 is a mutex, at which point it stops being a mutex? Note that 4 is my initial example.
- dist-epoch 11mo agoSounds like the Java synchronized class.
- the_gipsy 11mo agoNot at all. With rust you cannot accidentally leak a reference, and here's the killer: it guarantees these properties at compile time.
- masklinn 11mo agoNo. It’s not a property of the type so you can have multiple items under a mutex and you’re not at the mercy of whoever wrote it, it works fine with POD types, it does not force a lock / unlock on each method call (instead the compiler essentially ensures you hold the lock before you can access the data), and the borrow checker is there to ensure you can not leak any sort of sub-states, even though you can call all sorts of helpers which have no requirement to be aware of the locking. It’s what synchronized classes wish they had been, maybe.
- Nauxuron 11mo ago> You can't even access the data without locking the mutex. It's even nicer than that: you can actually access data without locking the mutex, because while you hold a mutable borrow to the mutex, Rust statically guarantees that no one else can acquire locks on the mutex. https://doc.rust-lang.org/std/sync/struct.Mutex.html#method.get_mut https://doc.rust-lang.org/std/sync/struct.Mutex.html#method....
- jstimpfle 11mo agoGiven a data item of non-thread safe type (i.e. not Mutex<T> etc), the borrow checker checks that there's only ever one mutable reference to it. This doesn't solve concurrency as it prevents multiple threads from even having the ability to access that data. Mutex is for where you have that ability, and ensures at runtime that accesses get serialized.
- dwattttt 11mo agoThe maybe unexpected point is that if you know you're the only one who has a reference to a Mutex (i.e. you have a &mut), you don't need to bother lock it; if no one else knows about the Mutex, there's no one else who could lock it. It comes up when you're setting things up and haven't shared the Mutex yet. This means no atomic operations or syscalls or what have you.
- jstimpfle 11mo agoDo you have an example? I don't program in Rust, but I imagine I'd rarely get into that situation. Either my variable is a local (in a function) in which case I can tell pretty easily whether I'm the only one accessing it. Or, the data is linked globally in a data structure and the only way to access it safely is by knowing exactly what you're doing and what the other threads are doing. How is Rust going to help here? I imagine it's only making the optimal thing harder to achieve. I can see that there are some cases where you have heap-data that is only visible in the current thread, and the borrow checker might be able to see that. But I can imagine that there are at least as many cases where it would only get in the way and probably nudge me towards unnecessary ceremony, including run-time overhead.
- indigo945 11mo agoThis doesn't solve the deadlock problem, however.
- torginus 11mo agoMutexes suffer from a host of problems, and imo are not a very good concurrency primitive - they were designed to turn single-threaded code into multi-threaded. With todays 8+ cores in most systems, usually a single point of contention quickly becomes a problem. They're liable to deadlocks/livelocks, and sometimes not only with other explicitly Mutex-like things (it might happen some library you use has a lock hidden deep inside). They're also often backed byOS primitives (with big overheads) with inconsistent behaviors between platforms (spinlocks, waiting etc). We've run into an issue with .NET, that their version of Mutex didn't wake up the blocked thread on Linux as fast as on Windows, meaning we needed about 100x the time to serve a request as the thread was sleeping too long. There are questions like when to use spinlocks and when to go to wait sleep, which unfortunately the developer has to answer. Not assigning blame here, just pointing out that threading primitives and behaviors don't translate perfectly between OSes. Multi-threading is hard, other solutions like queues suffer from issues like backpressure. That's why I'm skeptical about Rust's fearless concurrency promise - none of these bugs are solved by just figuring out data races - which are a huge issue, but not the only one.
- adwn 11mo agoYour view on mutex performance and overhead is outdated, at least for the major platforms: The Rust standard library mutex only requires 5 bytes, doesn't allocate, and only does a syscall on contention. The mutex implementation in the parking_lot library requires just 1 byte per mutex (and doesn't allocate and only does a syscall on contention). This enables very fine-grained, efficient locking and low contention.
- magicalhippo 11mo agoHow does it avoid cache contention with just a few bytes per mutex? That is, multiple mutex instances sharing a cache line. Say I have a structure with multiple int32 counters protected by their own mutex.
- deleted 11mo ago[deleted]
- kragen 11mo agoTraditionally traditionally, monitors were declared together with the data they contained, and the compiler enforced that the data was not accessed outside the monitor. Per Brinch Hansen wrote a rather bitter broadside against Java's concurrency model when it came out.