7 ms·
Inside Rust's std and parking_lot mutexes – who wins?
- pizlonator 11mo agoAuthor of the original WTF::ParkingLot here (what rust’s parking_lot is based on). I’m surprised that this only compared to std on one platform (Linux). The main benefit of parking lot is that it makes locks very small, which then encourages the use of fine grained locking. For example, in JavaScriptCore (ParkingLot’s first customer), we stuff a 2-bit lock into every object header - so if there is ever a need to do some locking for internal VM reasons on any object we can do that without increasing the size of the object
- nextaccountic 11mo agoHow can a parking_lot lock be less than 1 byte? does this uses unsafe? Rust in general doesn't support bit-level objects unless you cast things to [u8] and do some shifts and masking manually (that is, like C), which of course is wildly unsafe for data structures with safety invariants
- Conscat 11mo agoThis article elaborates how it works.
- scottlamb 11mo agoUnhelpful response. This cuongle.dev article does not answer nextaccountic's question, and neither do the webkit.org articles that describe the parking lot concept but not this Rust implementation. The correct answer appears to be that it's impossible: `parking_lot::RawMutex` has private storage that owns the entire byte and does not provide any accessor for the unused six bits. https://docs.rs/parking_lot/0.12.5/parking_lot/struct.RawMutex.html https://docs.rs/parking_lot/0.12.5/parking_lot/struct.RawMut... (unless there's somewhere else in the crate that provides an accessor for this but that'd be a weird interface) (or you just use transmute to "know" that it's one byte and which bits within the byte it actually cares about, but really don't do that) (slightly more realistically, you could probably use the `parking_lot_core::park` portion of the implementation and build your own equivalent of `parking_lot::RawMutex` on top of it) (or you send the `parking_lot` folks a PR to extend `parking_lot::RawMutex` with interface you want; it is open source after all)
- loeg 11mo agoThe two bit lock was specifically refering to the C++ WTF::ParkingLot (and the comment mentioning it explicitly said that). nextaccountic is confused.
- scottlamb 11mo agoNo. nextaccountic's comment and the cuongle.dev article are both talking about Rust. The Rust `parking_lot` implementation only uses two bits within a byte, but it doesn't provide a way for anything else to use the remaining six. pizlonator's comments mention both the (C++) WTF::ParkingLot and the Rust `parking_lot`, and they don't answer nextaccountic's question about the latter. > nextaccountic is confused. nextaccountic asked how this idea could be applied to this Rust implementation. That's a perfectly reasonable question. pizlonator didn't know the anwer. That's perfectly reasonable too. Conscat suggested the article would be helpful; that was wrong.
- loeg 11mo agonextaccountic replied to this original comment: https://news.ycombinator.com/item?id=46035698 https://news.ycombinator.com/item?id=46035698 Yes, nextaccountic's reply is confused about Rust vs C++ implementations. But the original mention was not talking about Rust.
- pizlonator 11mo ago> and neither do the webkit.org articles that describe the parking lot concept but not this Rust implementation The WebKit post explicitly talks about how you just need two bits to describe the lock state. > The correct answer appears to be that it's impossible: `parking_lot::RawMutex` has private storage that owns the entire byte and does not provide any accessor for the unused six bits. Not impossible. One way to do this is to just use parking_lot directly. In WebKit there’s a template mixin that lets you steal two bits for locking however you like. JavaScriptCore uses this to steal two bits from the indexing type byte (if I remember right)
- 11mo ago
- pizlonator 11mo agoOriginal post: https://webkit.org/blog/6161/locking-in-webkit/ https://webkit.org/blog/6161/locking-in-webkit/ Post that mentions the two bit lock: https://webkit.org/blog/7122/introducing-riptide-webkits-retreating-wavefront-concurrent-garbage-collector/ https://webkit.org/blog/7122/introducing-riptide-webkits-ret... I don’t know the details of the Rust port but I don’t imagine the part that involves the two bits to require unsafe, other than in the ways that any locking algorithm dances with unsafety in Rust (ownership relies on locking algorithms being correct)
- writebetterc 11mo agoThis is very similar to how Java's object monitors are implemented. In OpenJDK, the markWord uses two bits to describe the state of an Object's monitor (see markWord.hpp:55). On contention, the monitor is said to become inflated, which basically means revving up a heavier lock and knowing how to find it. I'm a bit disappointed though, I assumed that you had a way of only using 2 bits of an object's memory somehow, but it seems like the lock takes a full byte?
- zozbot234 11mo agoThe idea is that six bits in the byte are free to use as you wish. Of course you'll need to implement operations on those six bits as CAS loops (which nonetheless allow for any arbitrary RMW operation) to avoid interfering with the mutex state.
- pizlonator 11mo agoThe lock takes two bits. It’s just that if you use the WTF::Lock class the. You get a full byte simply because the smallest possible size of a class instance in C++ is one byte. But there’s a template mixing thing you can use to get it to be two bits (you tell the mixin which byte to steal the two bits from and which two bits). I suspend the same situation holds in the Rust port. I am very familiar with how Java does locks. This is different. Look at the ParkingLot/parking_lot API. It lets you do much more than just locks, and there’s no direct equivalent of what Java VMs call the inflated or fat lock. The closest thing is the on demand created queue keyed by address.
- bobbylarrybobby 11mo agoThe lock uses two bits but still takes up a whole (atomic) byte
- scottlamb 11mo ago> The main benefit of parking lot is that it makes locks very small, which then encourages the use of fine grained locking. For example, in JavaScriptCore (ParkingLot’s first customer), we stuff a 2-bit lock into every object header - so if there is ever a need to do some locking for internal VM reasons on any object we can do that without increasing the size of the object IMHO that's a very cool feature which is essentially wasted when using it as a `Mutex<InnerBlah>` because the mutex's size will get rounded up to the alignment of `InnerBlah`. And even when not doing that, afaict `parking_lot` doesn't expose a way to use the remaining six bits in `parking_lot::RawMutex`. I think the new std mutexes made the right choice to use a different design. > I’m surprised that this only compared to std on one platform (Linux). Can't speak for the author, but I suspect a lot of people really only care about performance under Linux. I write software that I often develop from a Mac but almost entirely deploy on Linux. (But speaking of Macs: std::mutex doesn't yet use futexes on macOS. Might happen soon. https://github.com/rust-lang/rust/pull/122408 https://github.com/rust-lang/rust/pull/122408)
- pizlonator 11mo ago> I suspect a lot of people really only care about performance under Linux Yeah this is true
- Rusky 11mo agoHypothetically Rust could make `Mutex<InnerBlah>` work with just two bits in the same way it makes `Option<&T>` the same size as `&T`. Annotate `InnerBlah` with the information about which bits are available and let `Mutex` use them.
- timClicks 11mo agoReferences only have a single bit available as a niche (the null byte), which Option makes use of for null pointer optimization (https://doc.rust-lang.org/std/option/index.html#representation https://doc.rust-lang.org/std/option/index.html#representati...). In principle, you Rust could create something like std::num::NonZero and its corresponding sealed trait ZeroablePrimitive to mark that two bits are unused. But that doesn't exist yet as far as I know.
- skitter 11mo agoI do the same in my toy JVM (to implement the reentrant mutex+condition variable that every Java object has), except I've got a rare deadlock somewhere because, as it turns out, writing complicated low level concurrency primitives is kinda hard :p
- kccqzy 11mo agoThere was a giant super-long GitHub issue about improving Rust std mutexes a few years back. Prior to that issue Rust was using something much worse, pthread_mutex_t. It explained the main reason why the standard library could not just adopt parking_lot mutexes: From https://github.com/rust-lang/rust/issues/93740 https://github.com/rust-lang/rust/issues/93740 > One of the problems with replacing std's lock implementations by parking_lot is that parking_lot allocates memory for its global hash table. A Rust program can define its own custom allocator, and such a custom allocator will likely use the standard library's locks, creating a cyclic dependency problem where you can't allocate memory without locking, but you can't lock without first allocating the hash table. > After some discussion, the consensus was to providing the locks as 'thinnest possible wrapper' around the native lock APIs as long as they are still small, efficient, and const constructible. This means SRW locks on Windows, and futex-based locks on Linux, some BSDs, and Wasm. > This means that on platforms like Linux and Windows, the operating system will be responsible for managing the waiting queues of the locks, such that any kernel improvements and features like debugging facilities in this area are directly available for Rust programs.
- wahern 11mo ago> Prior to that issue Rust was using something much worse, pthread_mutex_t Presumably you're referring to this description, from the Github Issue: > > On most platforms, these structures are currently wrappers around their pthread equivalent, such as pthread_mutex_t. These types are not movable, however, forcing us to wrap them in a Box, resulting in an allocation and indirection for our lock types. This also gets in the way of a const constructor for these types, which makes static locks more complicated than necessary. pthread mutexes are const-constructible in a literal sense, just not in the sense Rust requires. In C you can initialize a pthread_mutex_t with the PTHREAD_MUTEX_INITIALIZER initializer list instead of pthread_mutex_init, and at least with glibc there's no subsequent allocation when using the lock. But Rust can't do in-place construction[1] (i.e. placement new in C++ parlance), which is why Rust needs to be able to "move" the mutex. Moving a mutex is otherwise non-sensical once the mutex is visible--it's the address of the mutex that the locking is built around. The only thing you gain by not using pthread_mutex_t is a possible smaller lock--pthread_mutex_t has to contain additional members to support robust, recursive, and error checking mutexes, though altogether that's only 2 or 3 additional words because some are union'd. I guess you also gain the ability to implement locking, including condition variables, barriers, etc, however you want, though now you can't share those through FFI. [1] At least not without unsafe and some extra work, which presumably is a non-starter for a library type where you want to keep it all transparent.
- adzm 11mo agoThe original webkit blog post about parking lot mutex implementation is a great read https://webkit.org/blog/6161/locking-in-webkit/ https://webkit.org/blog/6161/locking-in-webkit/
- kouteiheika 11mo ago> Poisoning: Panic Safety in Mutexes This is one of the biggest design flaws in Rust's std, in my opinion. Poisoning mutexes can have its use, but it's very rare in practice. Usually it's a huge misfeature that only introduces problems. More often than not panicking in a critical section is fine[1], but on the other hand poisoning a Mutex is a very convenient avenue for a denial-of-service attack, since a poisoned Mutex will just completely brick a given critical section. I'm not saying such a project doesn't exist, but I don't think I've ever seen a project which does anything sensible with Mutex's `Poisoned` error besides ignoring it. It's always either an `unwrap` (and we know how well that can go [2]), or do the sensible thing and do this ridiculous song-and-dance: let guard = match mutex { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner() }; Suffice to say, it's a pain. So in a lot of projects when I need a mutex I just add `parking_lot`, because its performance is stellar, and it doesn't have the poisoning insanity to deal with. [1] -- obviously it depends on a case-by-case basis, but if you're using such a low level primitive you should know what you're doing [2] -- https://blog.cloudflare.com/18-november-2025-outage/#memory-preallocation https://blog.cloudflare.com/18-november-2025-outage/#memory-...
- thayne 11mo agoThere are cases where it is useful. I had a case where if the mutex was poisened it was possible to reset the lock to a safe state (by writing a new value to the locked content). Or you may want to drop some resource or restart some operation instead of panicing if it is poisoned. But I agree that the default behavior should be that the user doesn't have to worry about it.
- LegionMammal978 11mo ago> It's always either an `unwrap` (and we know how well that can go [2]) If a mutex has been poisoned, then something must have already panicked, likely in some other thread, so you're already in trouble at that point. It's fine to panic in a critical section if something's horribly wrong, the problem comes with blindly continuing after a panic in other threads that operate on the same data. In general, you're unlikely to know what that panic was, so you have no clue if the shared data might be incompletely modified or otherwise logically corrupted. In general, unless I were being careful to maintain fault boundaries between threads or tasks (the archetypical example being an HTTP server handling independent requests), I'd want a panic in one thread to cascade into stopping the program as soon as possible. I wouldn't want to swallow it up and keep using the same data like nothing's wrong.
- Shelby-Thomas 11mo ago[dead]
- kccqzy 11mo agoI will personally recommend that unless you are writing performance sensitive code*, don’t use mutexes at all because they are too low-level an abstraction. Use MPSC queues for example, or something like RCU. I find these abstractions much more developer friendly. *: You may be, since you are using Rust.
- jltsiren 11mo agoA mutex is a natural abstraction when there is exactly one of them. You have a bunch of tasks doing their own stuff, with shared mutable state behind the mutex. When you start thinking about using two mutexes, other abstractions often become more convenient.
- fpoling 11mo agoI have found out that mutex solutions are more maintainable and amendable without big redesigns compared with channels or RCU. Consider a simple case of single producer-single consumer. While one can use bounded channels to implement back-pressure, in practice when one wants to either drop messages or apply back-pressure based on message priority any solution involving channels will lead to pile of complex multi-channel solutions and select. With mutex the change will be a straightforward replace of a queue by a priority queue and an extra if inside the mutex.
- kccqzy 11mo agoA channel can be backed by a priority queue if you wish. It’s just an abstraction. The channel internally probably uses mutexes too; it’s just that it’s helpful not to see mutexes in application code.
- fpoling 11mo agoSurely one can abstract priority queue with mutexes into own data structure. However it will contain enough application-specific logic so a chance of reuse will be slim. So by directly working with mutexes one will have simpler code overall that can still be more easy to adapt to changing requirements. In general the problem with channels is that they are not flexible enough while being rather abstract. A better abstraction is message passing with one message queue per thread like in Erlang. IMO it can cover more cases before one needs mutexes. But even with that proper back pressure and rate limiting is hard.
- Barry-Perkins 11mo ago[dead]
- mgaunard 11mo agotl;dr: the implementation that is designed for fairness has lower standard deviation under contention, but otherwise performs slightly worse. Nothing too surprising.
- ballpug 11mo agoFor Cargo.toml, an error: invalid basic string, expected `"` for 10:11 std/parking_lot_mutexes. Sourcing VS, documentation indicates Python, C/C++, GitHubCopilot, and an Extension Pack for Java in top extensions. [1]: https://code.visualstudio.com/docs https://code.visualstudio.com/docs
- dcrazy 11mo agoFYI, Apple platforms have had futexes since iOS 17.4 and macOS 14.4: https://developer.apple.com/documentation/os/synchronization?language=objc https://developer.apple.com/documentation/os/synchronization... Unfortunately, you sacrifice the priority inversion avoidance you would otherwise get with os_unfair_lock.
- mrlonglong 11mo agoThe C++ example given in the article is not correct. In C++ a mutex can wrap the object being protected.
- tialaramex 11mo agoAlthough there are owning mutexes for C++ the C++ standard library does not provide such a thing. So the std::mutex used in the example is not an owning mutex and that example works and does what was described. One reason not to provide the owning mutex in C++ is that it isn't able to deliver similar guarantees to Rust because its type system isn't strong enough. Rust won't let you accidentally keep a reference to the protected object after unlocking, C++ will for example.
- questioner8216 11mo agoI am not very familiar with C++'s API, but I believe that you are right that the C++ example in the article is incorrect, though for a different reason, namely that RAII is supported also in C++. In C++, a class like std::lock_guard also provides "Automatic unlock". AFAICT, the article argues that only Rust's API provides that. { const std::lock_guard<std::mutex> lock(mtx); account.balance -= amount; account.transaction_count++; } // Automatically unlocked.
- aw1621107 11mo ago> In C++, a class like std::lock_guard also provides "Automatic unlock". AFAICT, the article argues that only Rust's API provides that. The issue isn't automatic unlocking. From the article: > The problem? Nothing stops you from accessing account without locking the mutex first. The compiler won’t catch this bug. i.e., a C++ compiler will happily compile code that modifies `account` without taking the lock first. Your lock_guard example suffers from this same issue. Nothing in the C++ stdlib provides an API that makes it impossible to access `account` without first taking the lock, and while you can write C++ classes that approximate the Rust API you can't quite reach the same level of robustness without external help.
- 11mo ago