Safe Optimistic Lock Coupling
databasearchitects.blogspot.com
databasearchitects.blogspot.com
The fundamental problem is that nothing prevents a reader from dereferencing a node the writer has free()'d. Fixing this requires either adding RCU or Hazard pointers.
They don’t actually free the memory, but rather put it back into the memory pool with an incremented version counter. It can later be re-used as a new node, the version counter _not_ being reset.
In LeanStore’s use case they either way have a fixed amount of memory they’d like to use (since it’s a page buffer on top of disk data) so there’s no actual need to give the memory back to the OS.
If you’d like to actually free the memory back it’s a lot easier since you can now treat this more of an infrequent operation. The cost of that will be amortized very well.
EDIT: They also describe another very cool idea: If the memory is allocated with mmap you can deallocate it by using madvise DONT_NEED. This will cause the version counter to be “0” which you can interpret as “invalid”. You can then deallocate it at a later point when you’re absolutely sure no threads could access it. You’re basically doing a two-stage deallocation where the first stage releases the memory, but the page information is kept until the second stage.
In my own experiments I confirmed the same thing. Just using explicit free lists / allocator techniques was far more performant.
And actually-existing LeanStore on github doesn't use it, anyways. And strangely never has (looking through git history). Even though the paper (2018) describes it. Maybe Leis or Alhamsi can explain.
Curious if Umbra and the CedarDB stuff that came from it do use this approach, too. I seem to recall the Umbra talk speaks about using it.
As a lazy "eventually reclaim when I've got free cycles" technique maybe ok? But revealing that the actual LeanStore C++ code never does it.
I definitely need to try this out, though.
How does this approach outperform RwLock (for arbitrary interpretations of outperform) and how does RwLock outperform this approach (ditto)?
In this article, the locks are mutually exclusive. Reads are not blocked by Writes, and vice-versa, but Writers would block against other Writers.
The downside is that Readers need to check which "version" of the data they read, to see if it was modified during the operation by a Writer--since Writers do not block for Readers like they do with RwLock.
It's elegant because, in most scenarios, Readers will not need to re-read because the version won't change mid-run between an optimistic read lock and its unlock. In the cases where that does happen, your code needs to handle retrying the read / loop logic, which is why the article argues for a compiler mechanism to enforce that check and avoid "foot gun" scenarios.
As someone else mentions, it's also difficult for anybody to free memory (which for many purposes is actually fine)