Skip to content

Commit 3ca8330

Browse files
authored
Improve guard usage pattern
- Use guard objects in a scope rather than if statement. - Updated docs, tests, and benchmarks.
1 parent bbe59a5 commit 3ca8330

File tree

7 files changed

+224
-110
lines changed

7 files changed

+224
-110
lines changed

README.md

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,19 @@ int main()
1717
{
1818
lockables::Guarded<int> value{100};
1919

20-
// The guard is a pointer like object that owns a lock on value.
21-
if (auto guard = value.with_exclusive()) {
20+
{
21+
// The guard is a pointer like object that owns a lock on value.
22+
auto guard = value.with_exclusive();
23+
2224
// Writer lock until guard goes out of scope.
2325
*guard += 10;
2426
}
2527

2628
int copy = 0;
27-
if (auto guard = value.with_shared()) {
29+
{
30+
// Reader lock.
31+
const auto guard = value.with_shared();
32+
2833
// Reader lock.
2934
copy = *guard;
3035
}
@@ -47,12 +52,14 @@ int main()
4752
{
4853
lockables::Guarded<std::vector<int>> value{1, 2, 3, 4, 5};
4954

50-
// The guard allows for multiple operations in the lock scope.
51-
if (auto guard = value.with_exclusive()) {
55+
// The guard allows for multiple operations in one locked scope.
56+
{
57+
auto guard = value.with_exclusive();
58+
5259
// sum = value[0] + ... + value[n - 1]
5360
const int sum = std::reduce(guard->begin(), guard->end());
5461

55-
// value = value + sum(value)
62+
// value[i] = value[i] + sum(value)
5663
std::transform(guard->begin(), guard->end(), guard->begin(),
5764
[sum](int x) { return x + sum; });
5865

@@ -96,30 +103,45 @@ int main()
96103

97104
## Anti-patterns: Do not do this!
98105

106+
Problem: Data race by keeping an unguarded pointer.
107+
99108
Solution: The user must not keep a pointer or reference to the guarded value
100109
outside the locked scope.
101110

102111
```cpp
103112
lockables::Guarded<int> value;
104113

105114
int* unguarded_pointer{};
106-
if (auto guard = value.with_exclusive()) {
115+
{
116+
auto guard = value.with_exclusive();
117+
107118
// No! User must not keep a pointer or reference outside the guarded
108119
// scope.
109120
unguarded_pointer = &(*guard);
110121
}
111122

112123
// No! Data race if another thread is accessing value.
113-
// *unguarded_pointer = 1;
124+
// *unguarded_pointer = -10;
125+
126+
// No! User must not keep a reference to the guarded value.
127+
int& unguarded_reference =
128+
lockables::with_exclusive([](int& x) -> int& { return x; }, value);
129+
130+
// No! Data race if another thread is accessing value.
131+
// unguarded_reference = -20;
114132
```
115133
134+
Problem: Deadlock with recursive guards.
135+
116136
Solution: A calling thread must not own the mutex prior to calling any of the
117137
locking functions.
118138
119139
```cpp
120140
lockables::Guarded<int> value;
121141
122-
if (auto guard = value.with_exclusive()) {
142+
{
143+
auto guard = value.with_exclusive();
144+
123145
// No! Deadlock since this thread already owns a lock on value.
124146
// auto recursive_reader = value.with_shared();
125147
@@ -131,6 +153,8 @@ if (auto guard = value.with_exclusive()) {
131153
}
132154
```
133155

156+
Problem: Deadlock with multiple guards.
157+
134158
Solution: To lock multiple values, use the ``with_exclusive`` function which
135159
avoids deadlock.
136160

benchmarks/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This project uses the excellent [Benchmark](https://github.com/google/benchmark)
44
library from Google. Most of the benchmarks compare ``std::mutex`` and
5-
``std::shared_metux`` performance over various numbers of reader and writer
5+
``std::shared_mutex`` performance over various numbers of reader and writer
66
threads.
77

88
For example, the test case named:

benchmarks/bench_guarded.cpp

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ void BM_Guarded_Shared(benchmark::State& state) {
66
lockables::Guarded<T, Mutex> value;
77
for (auto _ : state) {
88
T copy{};
9-
if (auto guard = value.with_shared()) {
9+
{
10+
const auto guard = value.with_shared();
1011
copy = *guard;
1112
}
1213

@@ -22,7 +23,8 @@ void BM_Guarded_Exclusive(benchmark::State& state) {
2223
lockables::Guarded<T, Mutex> value;
2324
for (auto _ : state) {
2425
T copy{};
25-
if (auto guard = value.with_exclusive()) {
26+
{
27+
auto guard = value.with_exclusive();
2628
copy = *guard;
2729
}
2830

@@ -38,8 +40,8 @@ void BM_Guarded_Multiple(benchmark::State& state) {
3840
lockables::Guarded<T, Mutex> value1;
3941
lockables::Guarded<T, Mutex> value2;
4042
for (auto _ : state) {
41-
auto copy = lockables::with_exclusive(
42-
[](const auto& x, const auto& y) { return x + y; }, value1, value2);
43+
T copy = lockables::with_exclusive(
44+
[](auto& x, auto& y) -> T { return x + y; }, value1, value2);
4345

4446
benchmark::DoNotOptimize(copy);
4547
}
@@ -58,15 +60,13 @@ struct BM_Guarded_Fixture : benchmark::Fixture {
5860
lockables::Guarded<int64_t, Mutex> value{};
5961

6062
void SetUp(const benchmark::State& state) override {
61-
if (auto guard = value.with_exclusive()) {
62-
*guard = 0;
63-
}
63+
auto guard = value.with_exclusive();
64+
*guard = 0;
6465
}
6566

6667
void TearDown(const benchmark::State& state) override {
67-
if (auto guard = value.with_exclusive()) {
68-
assert(*guard == state.iterations() * state.range(0));
69-
}
68+
auto guard = value.with_shared();
69+
assert(*guard == state.iterations() * state.range(0));
7070
}
7171

7272
void BenchmarkCase(benchmark::State& state) override {
@@ -84,7 +84,8 @@ struct BM_Guarded_Fixture : benchmark::Fixture {
8484
void RunWriter(benchmark::State& state) {
8585
for (auto _ : state) {
8686
int64_t copy{};
87-
if (auto guard = value.with_exclusive()) {
87+
{
88+
auto guard = value.with_exclusive();
8889
*guard += 1;
8990
copy = *guard;
9091
}
@@ -96,7 +97,8 @@ struct BM_Guarded_Fixture : benchmark::Fixture {
9697
void RunReader(benchmark::State& state) {
9798
for (auto _ : state) {
9899
int64_t copy{};
99-
if (auto guard = value.with_shared()) {
100+
{
101+
const auto guard = value.with_shared();
100102
copy = *guard;
101103
}
102104

include/lockables/guarded.hpp

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,18 @@
2323
Usage:
2424
2525
Guarded<int> value{9};
26-
if (auto guard = value.with_exclusive()) {
26+
{
2727
// Writer access. The mutex is locked until guard goes out of scope.
28+
auto guard = value.with_exclusive();
29+
2830
*guard += 10;
2931
}
3032
3133
int copy = 0;
32-
if (auto guard = value.with_shared()) {
34+
{
3335
// Reader access.
36+
auto guard = value.with_shared();
37+
3438
copy = *guard;
3539
// *guard += 10; // will not compile!
3640
}
@@ -80,15 +84,19 @@ class GuardedScope;
8084
Guarded<std::vector<int>> value{1, 2, 3, 4, 5};
8185
8286
// Reader with shared lock.
83-
if (const auto guard = value.with_shared()) {
84-
// Deference pointer like object GuardedScope<std::vector<int>>
87+
{
88+
const auto guard = value.with_shared();
89+
90+
// Deference pointer like object guard
8591
if (!guard->empty()) {
8692
int copy = guard->back();
8793
}
8894
}
8995
9096
// Writer with exclusive lock.
91-
if (auto guard = value.with_exclusive()) {
97+
{
98+
auto guard = value.with_exclusive();
99+
92100
guard->push_back(100);
93101
(*guard).clear();
94102
}
@@ -130,15 +138,19 @@ class Guarded {
130138
Usage:
131139
132140
Guarded<int> value;
133-
if (const auto guard = value.with_shared()) {
134-
const int copy = *guard;
141+
{
142+
const auto guard = value.with_shared();
143+
144+
const int copy = *guard;
135145
}
136146
137147
Guarded<std::vector<int>> list;
138-
if (const auto guard = list.with_shared()) {
139-
if (!guard->empty()) {
140-
const int copy = guard->back();
141-
}
148+
{
149+
const auto guard = list.with_shared();
150+
151+
if (!guard->empty()) {
152+
const int copy = guard->back();
153+
}
142154
}
143155
*/
144156
[[nodiscard]] shared_scope with_shared() const;
@@ -153,13 +165,18 @@ class Guarded {
153165
Usage:
154166
155167
Guarded<int> value;
156-
if (auto guard = value.with_exclusive()) {
157-
*guard = 10;
168+
{
169+
auto guard = value.with_exclusive();
170+
171+
*guard = 10;
158172
}
159173
160174
Guarded<std::vector<int>> list;
161-
if (auto guard = list.with_exclusive()) {
162-
guard->push_back(100);
175+
{
176+
auto guard = list.with_exclusive();
177+
178+
guard->push_back(100);
179+
guard->push_back(10);
163180
}
164181
*/
165182
[[nodiscard]] exclusive_scope with_exclusive();
@@ -190,16 +207,20 @@ auto Guarded<T, Mutex>::with_exclusive() -> exclusive_scope {
190207
return exclusive_scope{&value_, mutex_};
191208
}
192209

193-
/*
210+
/**
194211
The with_exclusive function provides access to one or more Guarded<T> objects
195212
from a user supplied callback.
196213
197214
Basic usage:
198215
199216
Guarded<int> value;
200-
with_exclusive([](int& x) {
201-
x += 10;
202-
}, value);
217+
218+
with_exclusive(
219+
[](int& x) {
220+
// Writer with exclusive lock on value.
221+
x += 10;
222+
},
223+
value);
203224
204225
The intent is to support locking of multiple Guarded<T> objects. The
205226
with_exclusive function relies on std::scoped_lock for deadlock avoidance.
@@ -209,10 +230,13 @@ auto Guarded<T, Mutex>::with_exclusive() -> exclusive_scope {
209230
Guarded<int> value1{1};
210231
Guarded<int> value2{2};
211232
212-
with_exclusive([](int& x, int& y) {
213-
x += y;
214-
y /= 2;
215-
}, value1, value2);
233+
with_exclusive(
234+
[](int& x, int& y) {
235+
// Writer with exclusive lock on value1 and value2.
236+
x += y;
237+
y /= 2;
238+
},
239+
value1, value2);
216240
217241
References:
218242
@@ -232,10 +256,10 @@ std::invoke_result_t<F, ValueTypes&...> with_exclusive(
232256
Type trait to select which lock is used in GuardedScope<T>.
233257
234258
Use these std lock types internally for shared access from readers.
235-
- std::scoped_lock<std::mutex> lock(...);
236-
- std::shared_lock<std::shared_mutex> lock(...);
259+
- std::scoped_lock<std::mutex>
260+
- std::shared_lock<std::shared_mutex>
237261
238-
Always use std::scoped_lock<Mutex> for writers.
262+
Always use std::scoped_lock<Mutex> for exclusive access from writers.
239263
240264
This means that if the user chooses std::mutex the shared and exclusive locks
241265
are the same type.

0 commit comments

Comments
 (0)