/src/trafficserver/include/tsutil/TsSharedMutex.h
Line | Count | Source |
1 | | /** @file |
2 | | |
3 | | A drop-in replacement for std::shared_mutex with guarantees against writer |
4 | | starvation. |
5 | | |
6 | | @section license License |
7 | | |
8 | | Licensed to the Apache Software Foundation (ASF) under one |
9 | | or more contributor license agreements. See the NOTICE file |
10 | | distributed with this work for additional information |
11 | | regarding copyright ownership. The ASF licenses this file |
12 | | to you under the Apache License, Version 2.0 (the |
13 | | "License"); you may not use this file except in compliance |
14 | | with the License. You may obtain a copy of the License at |
15 | | |
16 | | http://www.apache.org/licenses/LICENSE-2.0 |
17 | | |
18 | | Unless required by applicable law or agreed to in writing, software |
19 | | distributed under the License is distributed on an "AS IS" BASIS, |
20 | | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
21 | | See the License for the specific language governing permissions and |
22 | | limitations under the License. |
23 | | */ |
24 | | |
25 | | #pragma once |
26 | | |
27 | | #include <cstdio> |
28 | | #include <cstdlib> |
29 | | #include <pthread.h> |
30 | | #include "tsutil/Strerror.h" |
31 | | #include "tsutil/Assert.h" |
32 | | #include "tsutil/ts_thread_safety.h" |
33 | | |
34 | | #ifdef X |
35 | | #error "X preprocessor symbol defined" |
36 | | #endif |
37 | | |
38 | | #if !defined(__OPTIMIZE__) |
39 | | |
40 | | #define X(P) P |
41 | | |
42 | | #include <atomic> |
43 | | |
44 | | #else |
45 | | |
46 | | #define X(P) |
47 | | |
48 | | #endif |
49 | | |
50 | | namespace ts |
51 | | { |
52 | | // A class with the same interface as std::shared_mutex, but which is not prone to writer starvation. |
53 | | // |
54 | | class TS_CAPABILITY("shared_mutex") shared_mutex |
55 | | { |
56 | | public: |
57 | 2 | shared_mutex() {} |
58 | | |
59 | | // No copying or moving. |
60 | | // |
61 | | shared_mutex(shared_mutex const &) = delete; |
62 | | shared_mutex &operator=(shared_mutex const &) = delete; |
63 | | |
64 | | // The lock/unlock methods are the trusted implementation of this capability: |
65 | | // their bodies drive the raw pthread_rwlock_t, which some libc headers (e.g. |
66 | | // FreeBSD's <pthread.h>) annotate as a capability in its own right. Exempt the |
67 | | // bodies from analysis so only the capability contract on each signature is |
68 | | // checked; the actual data-race checking happens at the call sites. |
69 | | void |
70 | | lock() TS_ACQUIRE() TS_NO_THREAD_SAFETY_ANALYSIS |
71 | 0 | { |
72 | 0 | int error = pthread_rwlock_wrlock(&_lock); |
73 | 0 | if (error != 0) { |
74 | 0 | _call_fatal("pthread_rwlock_wrlock", &_lock, error); |
75 | 0 | } |
76 | 0 | X(_exclusive = true;) |
77 | 0 | } |
78 | | |
79 | | bool |
80 | | try_lock() TS_TRY_ACQUIRE(true) TS_NO_THREAD_SAFETY_ANALYSIS |
81 | 0 | { |
82 | 0 | int error = pthread_rwlock_trywrlock(&_lock); |
83 | 0 | if (EBUSY == error) { |
84 | 0 | return false; |
85 | 0 | } |
86 | 0 | if (error != 0) { |
87 | 0 | _call_fatal("pthread_rwlock_trywrlock", &_lock, error); |
88 | 0 | } |
89 | 0 | X(_exclusive = true;) |
90 | 0 |
|
91 | 0 | return true; |
92 | 0 | } |
93 | | |
94 | | void |
95 | | unlock() TS_RELEASE() TS_NO_THREAD_SAFETY_ANALYSIS |
96 | 0 | { |
97 | 0 | X(debug_assert(_exclusive);) |
98 | 0 | X(_exclusive = false;) |
99 | |
|
100 | 0 | _unlock(); |
101 | 0 | } |
102 | | |
103 | | void |
104 | | lock_shared() TS_ACQUIRE_SHARED() TS_NO_THREAD_SAFETY_ANALYSIS |
105 | 0 | { |
106 | 0 | int error = pthread_rwlock_rdlock(&_lock); |
107 | 0 | if (error != 0) { |
108 | 0 | _call_fatal("pthread_rwlock_rdlock", &_lock, error); |
109 | 0 | } |
110 | 0 | X(debug_assert(_shared >= 0);) |
111 | 0 | X(++_shared;) |
112 | 0 | X(debug_assert(_shared > 0);) |
113 | 0 | } |
114 | | |
115 | | bool |
116 | | try_lock_shared() TS_TRY_ACQUIRE_SHARED(true) TS_NO_THREAD_SAFETY_ANALYSIS |
117 | 0 | { |
118 | 0 | int error = pthread_rwlock_tryrdlock(&_lock); |
119 | 0 | if (EBUSY == error) { |
120 | 0 | return false; |
121 | 0 | } |
122 | 0 | if (error != 0) { |
123 | 0 | _call_fatal("pthread_rwlock_tryrdlock", &_lock, error); |
124 | 0 | } |
125 | 0 |
|
126 | 0 | X(++_shared;) |
127 | 0 |
|
128 | 0 | return true; |
129 | 0 | } |
130 | | |
131 | | void |
132 | | unlock_shared() TS_RELEASE_SHARED() TS_NO_THREAD_SAFETY_ANALYSIS |
133 | 0 | { |
134 | 0 | X(debug_assert(_shared > 0);) |
135 | 0 | X(--_shared;) |
136 | 0 | X(debug_assert(_shared >= 0);) |
137 | |
|
138 | 0 | _unlock(); |
139 | 0 | } |
140 | | |
141 | | ~shared_mutex() |
142 | 0 | { |
143 | 0 | int error = pthread_rwlock_destroy(&_lock); |
144 | 0 | if (error != 0) { |
145 | 0 | _call_fatal("pthread_rwlock_destroy", &_lock, error); |
146 | 0 | } |
147 | 0 | } |
148 | | |
149 | | using native_handle_type = pthread_rwlock_t; |
150 | | |
151 | | native_handle_type |
152 | | native_handle() |
153 | 0 | { |
154 | 0 | return _lock; |
155 | 0 | } |
156 | | |
157 | | private: |
158 | | void |
159 | | _unlock() TS_NO_THREAD_SAFETY_ANALYSIS |
160 | 0 | { |
161 | 0 | int error = pthread_rwlock_unlock(&_lock); |
162 | 0 | if (error != 0) { |
163 | 0 | _call_fatal("pthread_rwlock_unlock", &_lock, error); |
164 | 0 | } |
165 | 0 | } |
166 | | |
167 | | #if defined(__linux__) |
168 | | // Use the initializer that prevents writer starvation. |
169 | | // |
170 | | pthread_rwlock_t _lock = PTHREAD_RWLOCK_WRITER_NONRECURSIVE_INITIALIZER_NP; |
171 | | #else |
172 | | // Testing indicates that for MacOS/Darwin and FreeBSD, pthread rwlocks always prevent writer starvation. |
173 | | // |
174 | | pthread_rwlock_t _lock = PTHREAD_RWLOCK_INITIALIZER; |
175 | | #endif |
176 | | |
177 | | [[noreturn]] static void |
178 | | _call_fatal(char const *func_name, void *ptr, int errnum) noexcept |
179 | 0 | try { |
180 | 0 | fatal_error("{}({}) failed: {} ({})", func_name, ptr, Strerror(errnum).c_str(), errnum); |
181 | 0 | } catch (...) { |
182 | | // Formatting the message can throw (it allocates); abort with a minimal |
183 | | // breadcrumb rather than let an exception escape into a noexcept destructor. |
184 | 0 | fprintf(stderr, "shared_mutex: %s failed (%d); abort message formatting threw\n", func_name, errnum); |
185 | 0 | std::abort(); |
186 | 0 | } |
187 | | |
188 | | // In debug builds, make sure shared vs. exclusive locks and unlocks are properly paired. |
189 | | // |
190 | | X(std::atomic<bool> _exclusive{false};) |
191 | | X(std::atomic<int> _shared{0};) |
192 | | }; |
193 | | |
194 | | // RAII guards for ts::shared_mutex that carry Clang thread-safety capability |
195 | | // state. Prefer these over std::unique_lock / std::shared_lock in code annotated |
196 | | // for -Wthread-safety: the analysis does not reliably track the std wrappers |
197 | | // (see tsutil/ts_thread_safety.h). |
198 | | // |
199 | | class TS_SCOPED_CAPABILITY write_guard |
200 | | { |
201 | | public: |
202 | 0 | explicit write_guard(shared_mutex &m) TS_ACQUIRE(m) : _m(m) { _m.lock(); } |
203 | 0 | ~write_guard() TS_RELEASE() { _m.unlock(); } |
204 | | |
205 | | write_guard(write_guard const &) = delete; |
206 | | write_guard &operator=(write_guard const &) = delete; |
207 | | |
208 | | private: |
209 | | shared_mutex &_m; |
210 | | }; |
211 | | |
212 | | class TS_SCOPED_CAPABILITY read_guard |
213 | | { |
214 | | public: |
215 | 0 | explicit read_guard(shared_mutex &m) TS_ACQUIRE_SHARED(m) : _m(m) { _m.lock_shared(); } |
216 | | // A scoped-capability destructor uses the plain release form even for a shared acquire. |
217 | 0 | ~read_guard() TS_RELEASE() { _m.unlock_shared(); } |
218 | | |
219 | | read_guard(read_guard const &) = delete; |
220 | | read_guard &operator=(read_guard const &) = delete; |
221 | | |
222 | | private: |
223 | | shared_mutex &_m; |
224 | | }; |
225 | | |
226 | | } // end namespace ts |
227 | | |
228 | | #undef X |