forked from abseil/abseil.github.io
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Export of internal doc changes to C++ Tips:
rpc://team/absl-team/Abseil-Docs Included changes: 647738412(shreck): Internal change 647719902(shreck): Fix malformed tag 647718693(shreck): Internal change 645068012(Abseil Team): Internal change 644392454(jdennett): Internal change 643172690(Abseil Team): Internal change 640698746(jdennett): Internal change 634844740(Abseil Team): Internal change 628148022(Abseil Team): Internal change 628136192(Abseil Team): Internal change 628059126(Abseil Team): Internal change 620995142(Abseil Team): Internal change PiperOrigin-RevId: 647738412 Change-Id: I9d5ba5526c6fef527527f69938bcc446536122e6
- Loading branch information
Showing
5 changed files
with
116 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
--- | ||
title: "Tip of the Week #197: Reader Locks Should Be Rare" | ||
layout: tips | ||
sidenav: side-nav-tips.html | ||
published: true | ||
permalink: tips/197 | ||
type: markdown | ||
order: "197" | ||
--- | ||
|
||
Originally posted as TotW #197 on July 29, 2021 | ||
|
||
*By [Titus Winters](mailto:titus@cs.ucr.edu)* | ||
|
||
Updated 2024-04-01 | ||
|
||
Quicklink: [abseil.io/tips/197](https://abseil.io/tips/197) | ||
|
||
|
||
“Ah, how good it is to be among people who are reading.” - *Rainer Maria Rilke* | ||
|
||
The `absl::Mutex` class has supported two styles of locking for many years now: | ||
|
||
* Exclusive locks, in which exactly one thread holds the lock. | ||
* Shared locks, which have two modes. If they are held “for writing” they use | ||
an exclusive lock, but they also have a different mode in which many threads | ||
can hold the lock “for reading.” | ||
|
||
How can a shared lock be acceptable? Isn’t the whole point of having a lock to | ||
gain exclusive access to an object? The perceived value in shared locks is when | ||
we need read-only access to the underlying data/objects. Remember that we get | ||
data races and API races when two threads access the same data without | ||
synchronization, and at least one of those accesses is a write. If we use a | ||
shared-lock when many threads only need to read data, and always use exclusive | ||
locks when writing data, we can avoid contention among the readers and still | ||
avoid data and API races. | ||
|
||
To support this, `absl::Mutex` has both `Mutex::Lock()` (and | ||
`Mutex::WriterLock()`, an alternate name for the same exclusive behavior) as | ||
well as `Mutex::ReaderLock()`. From reading through those interfaces, you might | ||
think that we should prefer `ReaderLock()` when we’re only reading from the data | ||
protected by the lock. | ||
|
||
In many cases you’d be wrong. | ||
|
||
### ReaderLock Is Slow | ||
|
||
`ReaderLock` inherently does more bookkeeping and requires more overhead than a | ||
standard exclusive lock. As a result, in many cases using the more specialized | ||
form (shared locks) is actually a performance loss, as we have to do quite a bit | ||
more work in the lock machinery itself. This cost is minor in the absence of | ||
contention, but `ReaderLock` underperforms `Lock` under contention for short | ||
critical sections. Without contention, the value `ReaderLock` provides is less | ||
significant in the first place. | ||
|
||
Consider the logic in an exclusive lock vs. a shared lock. A shared lock | ||
generally must also have an exclusive lock mode - if there are no writers, no | ||
data race can occur, and thus there is no need for locking in the first place. | ||
Shared locking is therefore inherently more complex, requiring checks on whether | ||
other readers hold locks, or modifications to the (atomic) count of readers, | ||
etc. | ||
|
||
### When are Shared Locks Useful? | ||
|
||
Shared locks are primarily a benefit when the lock is going to be held for a | ||
comparatively long time and it's likely that multiple readers will concurrently | ||
obtain the *shared* lock. For example, if you’re going to do a lot of work while | ||
holding the lock (e.g. iterating over a large container, not just doing a single | ||
lookup), then a shared locking scheme may be valuable. The dominant question is | ||
not “am I writing to the data”, it’s “how long do I expect the lock to be held | ||
by readers (compared to how long it takes to acquire the lock)?” | ||
|
||
<pre class="prettyprint lang-cpp bad-code"> | ||
// This is bad - the amount of work done under the lock is insignificant. | ||
// The added complexity of using reader locks is going to cost more in aggregate | ||
// than the contention saved by having multiple threads able to call this | ||
// function concurrently. | ||
int Foo::GetElementSize() const { | ||
absl::ReaderMutexLock l(&lock_); | ||
return element_size_; | ||
} | ||
</pre> | ||
|
||
Even when the amount of computation performed under a lock is larger, and reader | ||
locks become more useful, we often find we have better special-case interfaces | ||
to avoid contention entirely - see https://abseil.io/fast and | ||
https://abseil.io/docs/cpp/guides/synchronization for more. RCU (“Read Copy | ||
Update”) abstractions provide a particularly common solution here, making the | ||
read path essentially free. | ||
|
||
### What Should We Do? | ||
|
||
Be on the lookout for use of `ReaderLock` - the overwhelming majority of uses of | ||
it are actually a pessimization … but we can’t statically determine that | ||
definitively to rewrite code to use exclusive locking instead. (Reasoning about | ||
concurrency properties in C++ is still too hard for most refactoring work.) | ||
|
||
If you spot `ReaderLock`, especially new uses of it, try to ask “Is the | ||
computation under this lock often long?” If it’s just looking up a value in a | ||
container, an exclusive lock is almost certainly a better solution. | ||
|
||
In the end, profiling may be the only way to be sure - contention tracking is | ||
particularly valuable here. |