Is it okay to use Mutex<bool>?

In that case, LMDB would allow the user to perform an operation on txn2 that happens-before an operation on txn1, since the txn1 thread sees both transactions as valid simultaneously. (I'm using "beginning" to refer to the mdb_txn_begin() call, and "end" to refer to the mdb_txn_{commit, abort}() call.) If it were able to do this in a way that actually worked, then I don't think the transaction pointers could really be considered unique at all.

That same amount of thinking and reasoning is necessary to show that the Mutex<bool> solution is also sound under the weaker assumption. The argument is nearly the same, we just use the happens-before edge between unlocking it and locking it. In particular, in the first scenario, close_cursors() locks the mutex before assert_txn_backend(), so assert_txn_backend() reads true and correctly panics. In the second scenario, assert_txn_backend() locks the mutex before close_cursors(). Then, the beginning of txn2 happens-before the mutex unlock in assert_txn_backend(), which synchronizes-with the mutex lock in close_cursors(), which happens-before the end of txn1; therefore, the second scenario requires the beginning of txn2 to happen-before the end of txn1, which we are assuming cannot occur. Thus, either txn2 is txn1 and assert_txn_backend() reads false (by the first possibility), or txn2 is not txn1 and assert_txn_backend() reads true (by the first scenario in the second possibility).

I suppose that the main reason AtomicBool with SeqCst is generally treated as sufficient is that it's not likely in practice for something to require a Release load or an Acquire store that a Mutex would be able to simulate. In this case, Mutex<bool> would only help over Relaxed operations if the weaker assumption were satisfied and the stronger assumption were violated, which would be a very odd architecture for such an API.

Hmmm, if that would hold in the general case, then it would mean Mutex<bool> is (generally) an indication for considering replacing it with a relaxed atomic, i.e. a potential "anti-pattern". Not sure if the example discussed here is representative, and also not sure if your statement was meant in general or in regard to this particular example.

In this particular case, a Relaxed atomic would work; from what I've seen, everyday Release stores and Acquire loads are generally seen as sufficient for the general case. (I recall hearing something to the effect that spinlocks are the one of the only valid use cases of a load synchronizing-with a store.) But I think Mutex<bool> still has a place, if you actually need to lock it for some amount of time while other threads block on it.

I still didn't work with AtomicBool, but for what I Understand at a first glance is that it is perfect for implementing spin-locks. On the other hand Mutex implements a monitor with an access queue that serialized the concurrent threads. Then, they have different purposes and are not equivalent at all.

I'm not sure about terminology, but I think a spin-lock is some sort of mutex (which consumes CPU while waiting though). Both a spin-lock and an ordinary mutex use atomics to synchronize, while the latter parks the thread and ensures that it's woken up when the lock becomes available.

I didn't read Mara Bos' book on Rust atomics and locks yet, but she writes here:

A spin lock is a mutex […]. Attempting to lock an already locked mutex will result in busy-looping or spinning: […]. This can waste processor cycles,

And:

[…] a regular mutex […] will put your thread to sleep when the mutex is already locked.

So what makes a mutex a mutex is the way it uses atomics for synchronization. Instead of using an atomic directly to store the value to which you want to have synchronized access to, the atomics are used to store the lock-state (and you store the variable which you want to access elsewhere).


In regard to the example of a Mutex<bool> it means that a Mutex<bool> consists (at least) of two atomics variables: one used for synchronization (an atomic) and one to store the actual boolean value (e.g. an UnsafeCell).

Yes, this is exactly the point. A spin-lock is non-blocking while providing mutual exclusive access to its value. Mutex provides a semaphore (https://www.cs.utexas.edu/users/EWD/transcriptions/EWD01xx/EWD123.html), where all threads waiting for accessing to the object Mutex synchronizes are queued. Interesting enough that at hardware level Mutex is implemented using a spin-lock.

After this long discussion, I decided to actually use the AtomicBool approach (source). In that context, I also invested some effort to add SAFETY comments to all uses of unsafe in mmtkvdb's code. I found it very helpful to enable the unsafe_op_in_unsafe_fn lint.

Adding the SAFETY comments, I found several soundness issues (critical bug fixes in mmtkvdb 0.14.1). It turned out that doing FFI calls in a sound way can be a really hard task. Especially LMDB comes with a lot of preconditions combined with sometimes-automatic releasing of resources. One of the more twisted examples from the LMDB API specification:

mdb_cursor_open():

A cursor cannot be used when its database handle is closed. Nor when its transaction has ended, except with mdb_cursor_renew(). It can be discarded with mdb_cursor_close(). A cursor in a write-transaction can be closed before its transaction ends, and will otherwise be closed when its transaction ends. A cursor in a read-only transaction must be closed explicitly, before or after its transaction ends. It can be reused with mdb_cursor_renew() before finally closing it.

Note: Earlier documentation said that cursors in every transaction were closed when the transaction committed or aborted.

:face_with_spiral_eyes:

What I (think I) learned from this discussion and from reviewing all my unsafe uses:

  • Mutex<bool> gives guarantees over AtomicBool. Often, the synchronization properties of Mutex<bool> might not be needed. But if you need them and you want to avoid using a Mutex, then you'll have to use more complex patterns using AtomicBool, rather than simply using SeqCst. SeqCst loads and stores most likely will not give you what you need. And remember: SeqCst only gives any advantage over Acquire/Release/AcqRel if there is more than one atomic involved!
  • Sometimes, synchronization happens implicitly. An example is malloc/free in C. But there may be other cases, e.g. Tokio's Notify, which may result in synchronization (see also this post by me in "A flag type that supports waiting asynchronously"). I think many APIs don't explicitly specify when or if such synchronization takes place. Being careful about that, one might end up using a lot of unnecessary extra-synchronization (as in my case of using Mutex<bool> where it (likely) wasn't necessary).
  • C API specifications, in particular, are often underspecified. You might need to make certain (reasonable) assumptions that aren't explicitly documented.
  • Writing sound unsafe code in Rust is harder than I imagined. Explicitly writing down the reason why each and every unsafe block is sound can be helpful, even if it seems "obvious" at a first glance. Using the unsafe_op_in_unsafe_fn lint is a valuable tool as well.
  • Of course, Mutex<bool> is a safe fallback. If you use that, you don't need to worry about synchronization. You might still endup with deadlocks though.
2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.