3 ms·
Any thoughts on the best way to express global locks in Rust? A classic example is a set of bank accounts, atomically transacting with each other. Fine-grained
by ridiculous_fish 2y ago
Any thoughts on the best way to express global locks in Rust?
A classic example is a set of bank accounts, atomically transacting with each other. Fine-grained per-account locking is possible, but risks deadlock due to lock ordering inversion. A simple solution is to replace per-account locks with a singleton global lock, covering all accounts. Any transaction must first acquire this lock, and now deadlock is impossible.
But this is an awkward fit for Rust, whose locks want to own the data they protect. What's the best way to express a global lock, enforcing that certain data may only be accessed while the global lock is held?
- lucasyvas 2y agoIf I’m understanding correctly can’t you lock on some other account criteria, like IDs? A Mutex does have to own the data but you can still decide what it owns. When you lock, the returned MutexGuard is owned, so you can pass it around or return it and it’s only dropped when it ultimately goes out of scope.
- returningfory2 2y agoMaybe define a new type in a such way that only 1 value of that type can be instantiated, and then put that unique value in a mutex. Any function that needs to hold the global lock accepts a value of the new type. Calling the function involves providing the value, which is proof that you are holding the mutex.
- chc4 2y agoUse ghost_cell/qcell, and put the QCellOwner in a mutex. Acquiring the cell owner grants the ability to access the data, but doesn't tie ownership.
- tialaramex 2y agoI don't think this is an awkward fit. There's no reason it should be Mutex<CustomerBankAccount> it can be Mutex<MyTransferToken> or indeed Mutex<()> if you're really sure you don't actually want to lock any data. One piece of advice I'd suggest is, write APIs which take that MyTransferToken to signify that you must take the lock before calling them, it can be a unit type (a Zero Size type, a struct with no members) at the start if you like, but I suspect you'll find that across several functions which take that MyTransferToken you realise actually the data I was going to put in a separate parameter is really always accompanying that token, and so it might as well go inside the the MyTransferToken and before you know it your unit type that was just to ensure correct locking is in fact an object with important data protected by the lock.
- duped 2y agoYou would write Mutex<HashMap<CustomerID, Account>> Instead of HashMap<CustomerID, Mutex<Account>> Using a singleton is kind of overkill. But in the real world you'd probably use a transactional database for this (there are a few in memory options) with retries.
- saghm 2y agoMy first instinct would be to use a static instance of `LazyLock`[1] wrapping the data (or `OnceLock`[2] if needed). `LazyLock` only just got stabilized, and `OnceLock` only a bit less recently, but both have equivalents available via the `once_cell`[3] package for a while. [1]: https://doc.rust-lang.org/std/sync/struct.LazyLock.html https://doc.rust-lang.org/std/sync/struct.LazyLock.html [2]: https://doc.rust-lang.org/std/sync/struct.OnceLock.html https://doc.rust-lang.org/std/sync/struct.OnceLock.html [3]: https://crates.io/crates/once_cell https://crates.io/crates/once_cell