userver: userver/compiler/thread_local.hpp Source File
⚠️ This is the documentation for an old userver version. Click here to switch to the latest version.
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages Concepts
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 &&
13 (__clang_major__ >= 13 || !defined(__clang__) && __GNUC__ >= 9)
14// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
15#define USERVER_IMPL_UNEVALUATED_LAMBDAS
16#endif
17
18USERVER_NAMESPACE_BEGIN
19
20namespace compiler {
21
22namespace impl {
23
24bool AreCoroutineSwitchesAllowed() noexcept;
25
26void IncrementLocalCoroutineSwitchBans() noexcept;
27
28void DecrementLocalCoroutineSwitchBans() noexcept;
29
30#ifdef USERVER_IMPL_UNEVALUATED_LAMBDAS
31template <typename T, typename Factory = decltype([] { return T{}; })>
32using UniqueDefaultFactory = Factory;
33#else
34template <typename T>
35struct UniqueDefaultFactory final {
36 static_assert(!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#endif
42
43} // namespace impl
44
45/// @brief The scope that grants access to a thread-local variable.
46///
47/// @see compiler::ThreadLocal
48template <typename VariableType>
49class ThreadLocalScope final {
50 public:
51 ThreadLocalScope(ThreadLocalScope&&) = delete;
52 ThreadLocalScope& operator=(ThreadLocalScope&&) = delete;
53 ~ThreadLocalScope();
54
55 /// Access the thread-local variable.
56 VariableType& operator*() & noexcept USERVER_IMPL_LIFETIME_BOUND;
57
58 /// Access the thread-local variable.
59 VariableType* operator->() & noexcept USERVER_IMPL_LIFETIME_BOUND;
60
61 /// @cond
62 explicit ThreadLocalScope(VariableType& variable) noexcept;
63
64 // Store ThreadLocalScope to a variable before using.
65 VariableType& operator*() && noexcept = delete;
66
67 // Store ThreadLocalScope to a variable before using.
68 VariableType* operator->() && noexcept = delete;
69 /// @endcond
70
71 private:
72 static_assert(!std::is_reference_v<VariableType>);
73 static_assert(!std::is_const_v<VariableType>);
74
75 VariableType& variable_;
76};
77
78/// @brief Creates a unique thread-local variable that can be used
79/// in a coroutine-safe manner.
80///
81/// Thread-local variables are known to cause issues when used together
82/// with userver coroutines:
83///
84/// * https://github.com/userver-framework/userver/issues/235
85/// * https://github.com/userver-framework/userver/issues/242
86///
87/// Thread-local variables created through this class are protected against
88/// these issues.
89///
90/// Example usage:
91///
92/// @snippet compiler/thread_local_test.cpp sample definition
93/// @snippet compiler/thread_local_test.cpp sample
94///
95/// The thread-local variable is value-initialized.
96///
97/// In C++17 mode, or if you need to initialize the variable with some
98/// arguments, the ThreadLocal should be passed a capture-less lambda that
99/// constructs the variable. Example:
100///
101/// @snippet compiler/thread_local_test.cpp sample factory
102///
103/// Once acquired through @a Use, the reference to the thread-local variable
104/// should not be returned or otherwise escape the scope
105/// of the ThreadLocalScope object. An example of buggy code:
106///
107/// @code
108/// compiler::ThreadLocal<std::string> local_buffer;
109///
110/// std::string_view PrepareBuffer(std::string_view x, std::string_view y) {
111/// const auto buffer = local_buffer.Use();
112/// buffer->clear();
113/// buffer->append(x);
114/// buffer->append(y);
115/// return *buffer; // <- BUG! Reference to thread-local escapes the scope
116/// }
117/// @endcode
118///
119/// Do not store a reference to the thread-local object in a separate variable:
120///
121/// @code
122/// compiler::ThreadLocal<std::string> local_buffer;
123///
124/// void WriteMessage(std::string_view x, std::string_view y) {
125/// auto buffer_scope = local_buffer.Use();
126/// // Code smell! This makes it more difficult to see that `buffer` is
127/// // a reference to thread-local at a glance.
128/// auto& buffer = *buffer_scope;
129/// buffer.clear();
130/// // ...
131/// }
132/// @endcode
133///
134/// Until the variable name goes out of scope, userver engine synchronization
135/// primitives and clients (web or db) should not be used.
136template <typename VariableType,
137 typename Factory = impl::UniqueDefaultFactory<VariableType>>
138class ThreadLocal final {
139 static_assert(std::is_empty_v<Factory>);
140 static_assert(
141 std::is_same_v<VariableType, std::invoke_result_t<const Factory&>>);
142
143 public:
144 USERVER_IMPL_CONSTEVAL ThreadLocal() : factory_(Factory{}) {}
145
146 USERVER_IMPL_CONSTEVAL /*implicit*/ ThreadLocal(Factory factory)
147 : factory_(factory) {}
148
149 ThreadLocalScope<VariableType> Use() {
150 return ThreadLocalScope<VariableType>(impl::ThreadLocal(factory_));
151 }
152
153 private:
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)
163 -> ThreadLocal<std::invoke_result_t<const Factory&>, Factory>;
164
165template <typename VariableType>
166ThreadLocalScope<VariableType>::ThreadLocalScope(
167 VariableType& variable) noexcept
168 : variable_(variable) {
169#ifndef NDEBUG
170 impl::IncrementLocalCoroutineSwitchBans();
171#endif
172}
173
174template <typename VariableType>
175ThreadLocalScope<VariableType>::~ThreadLocalScope() {
176#ifndef NDEBUG
177 impl::DecrementLocalCoroutineSwitchBans();
178#endif
179}
180
181template <typename VariableType>
182VariableType& ThreadLocalScope<VariableType>::operator*() & noexcept //
184 return variable_;
185}
186
187template <typename VariableType>
188VariableType* ThreadLocalScope<VariableType>::operator->() & noexcept //
190 return &**this;
191}
192
193} // namespace compiler
194
195USERVER_NAMESPACE_END