string_view
operator+
vs. StrCat()
absl::Status
std::bind
absl::optional
and std::unique_ptr
absl::StrFormat()
make_unique
and private
Constructors.bool
explicit
= delete
)switch
Statements Responsibly= delete
AbslHashValue
and Youcontains()
std::optional
parametersif
and switch
statements with initializersinline
Variablesstd::unique_ptr
Must Be MovedAbslStringify()
vector.at()
auto
for Variable DeclarationsOriginally posted as TotW #197 on July 29, 2021
Updated 2024-04-01
Quicklink: 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:
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
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.
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)?”
// 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_; }
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.
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.