userver: userver/compiler/thread_local.hpp Source File
Loading...
Searching...
No Matches
thread_local.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/compiler/thread_local.hpp
4/// @brief Utilities for using thread-local variables in a coroutine-safe way
5
6#include <type_traits>
7
8#include <userver/compiler/impl/constexpr.hpp>
9#include <userver/compiler/impl/lifetime.hpp>
10#include <userver/compiler/impl/tls.hpp>
11
12#if __cplusplus >= 202002L && (__clang_major__ >= 13 || !defined(__clang__) && __GNUC__ >= 9)
13// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
14#define USERVER_IMPL_UNEVALUATED_LAMBDAS
15#endif
16
17USERVER_NAMESPACE_BEGIN
18
19namespace compiler {
20
21namespace impl {
22
23bool AreCoroutineSwitchesAllowed() noexcept;
24
25void IncrementLocalCoroutineSwitchBans() noexcept;
26
27void DecrementLocalCoroutineSwitchBans() noexcept;
28
29#ifdef USERVER_IMPL_UNEVALUATED_LAMBDAS
30template <typename T, typename Factory = decltype([] { return T{}; })>
31using UniqueDefaultFactory = Factory;
32#else
33template <typename T>
34struct UniqueDefaultFactory final {
35 static_assert(
36 !sizeof(T),
37 "Defaulted syntax for compiler::ThreadLocal is unavailable on "
38 "your compiler. Please use the lambda-factory syntax, see the "
39 "documentation for compiler::ThreadLocal."
40 );
41};
42#endif
43
44} // namespace impl
45
46/// @brief The scope that grants access to a thread-local variable.
47///
48/// @see compiler::ThreadLocal
49template <typename VariableType>
50class ThreadLocalScope final {
51public:
52 ThreadLocalScope(ThreadLocalScope&&) = delete;
53 ThreadLocalScope& operator=(ThreadLocalScope&&) = delete;
54 ~ThreadLocalScope();
55
56 /// Access the thread-local variable.
57 VariableType& operator*() & noexcept USERVER_IMPL_LIFETIME_BOUND;
58
59 /// Access the thread-local variable.
60 VariableType* operator->() & noexcept USERVER_IMPL_LIFETIME_BOUND;
61
62 /// @cond
63 explicit ThreadLocalScope(VariableType& variable) noexcept;
64
65 // Store ThreadLocalScope to a variable before using.
66 VariableType& operator*() && noexcept = delete;
67
68 // Store ThreadLocalScope to a variable before using.
69 VariableType* operator->() && noexcept = delete;
70 /// @endcond
71
72private:
73 static_assert(!std::is_reference_v<VariableType>);
74 static_assert(!std::is_const_v<VariableType>);
75
76 VariableType& variable_;
77};
78
79/// @brief Creates a unique thread-local variable that can be used
80/// in a coroutine-safe manner.
81///
82/// Thread-local variables are known to cause issues when used together
83/// with userver coroutines:
84///
85/// * https://github.com/userver-framework/userver/issues/235
86/// * https://github.com/userver-framework/userver/issues/242
87///
88/// Thread-local variables created through this class are protected against
89/// these issues.
90///
91/// Example usage:
92///
93/// @snippet compiler/thread_local_test.cpp sample definition
94/// @snippet compiler/thread_local_test.cpp sample
95///
96/// The thread-local variable is value-initialized.
97///
98/// In C++17 mode, or if you need to initialize the variable with some
99/// arguments, the ThreadLocal should be passed a capture-less lambda that
100/// constructs the variable. Example:
101///
102/// @snippet compiler/thread_local_test.cpp sample factory
103///
104/// Once acquired through @a Use, the reference to the thread-local variable
105/// should not be returned or otherwise escape the scope
106/// of the ThreadLocalScope object. An example of buggy code:
107///
108/// @code
109/// compiler::ThreadLocal<std::string> local_buffer;
110///
111/// std::string_view PrepareBuffer(std::string_view x, std::string_view y) {
112/// const auto buffer = local_buffer.Use();
113/// buffer->clear();
114/// buffer->append(x);
115/// buffer->append(y);
116/// return *buffer; // <- BUG! Reference to thread-local escapes the scope
117/// }
118/// @endcode
119///
120/// Do not store a reference to the thread-local object in a separate variable:
121///
122/// @code
123/// compiler::ThreadLocal<std::string> local_buffer;
124///
125/// void WriteMessage(std::string_view x, std::string_view y) {
126/// auto buffer_scope = local_buffer.Use();
127/// // Code smell! This makes it more difficult to see that `buffer` is
128/// // a reference to thread-local at a glance.
129/// auto& buffer = *buffer_scope;
130/// buffer.clear();
131/// // ...
132/// }
133/// @endcode
134///
135/// Until the variable name goes out of scope, userver engine synchronization
136/// primitives and clients (web or db) should not be used.
137template <typename VariableType, typename Factory = impl::UniqueDefaultFactory<VariableType>>
138class ThreadLocal final {
139 static_assert(std::is_empty_v<Factory>);
140 static_assert(std::is_same_v<VariableType, std::invoke_result_t<const Factory&>>);
141
142public:
143 USERVER_IMPL_CONSTEVAL ThreadLocal()
144 : factory_(Factory{})
145 {}
146
147 USERVER_IMPL_CONSTEVAL /*implicit*/ ThreadLocal(Factory factory)
148 : factory_(factory)
149 {}
150
151 ThreadLocalScope<VariableType> Use() { return ThreadLocalScope<VariableType>(impl::ThreadLocal(factory_)); }
152
153private:
154 // The ThreadLocal instance should have static storage duration. Still, if a
155 // user defines it as a local variable or even a thread_local variable, it
156 // should be harmless in practice, because ThreadLocal is an empty type,
157 // mainly used to store the `FactoryFunc` template parameter.
158 Factory factory_;
159};
160
161template <typename Factory>
162ThreadLocal(Factory factory) -> ThreadLocal<std::invoke_result_t<const Factory&>, Factory>;
163
164template <typename VariableType>
165ThreadLocalScope<VariableType>::ThreadLocalScope(VariableType& variable) noexcept : variable_(variable) {
166#ifndef NDEBUG
167 impl::IncrementLocalCoroutineSwitchBans();
168#endif
169}
170
171template <typename VariableType>
172ThreadLocalScope<VariableType>::~ThreadLocalScope() {
173#ifndef NDEBUG
174 impl::DecrementLocalCoroutineSwitchBans();
175#endif
176}
177
178template <typename VariableType>
179VariableType& ThreadLocalScope<VariableType>::operator*() & noexcept //
180USERVER_IMPL_LIFETIME_BOUND {
181 return variable_;
182}
183
184template <typename VariableType>
185VariableType* ThreadLocalScope<VariableType>::operator->() & noexcept //
186USERVER_IMPL_LIFETIME_BOUND {
187 return &**this;
188}
189
190} // namespace compiler
191
192USERVER_NAMESPACE_END