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/lifetime.hpp>
9#include <userver/compiler/impl/tls.hpp>
10
11USERVER_NAMESPACE_BEGIN
12
13namespace compiler {
14
15namespace impl {
16
17bool AreCoroutineSwitchesAllowed() noexcept;
18
19void IncrementLocalCoroutineSwitchBans() noexcept;
20
21void DecrementLocalCoroutineSwitchBans() noexcept;
22
23} // namespace impl
24
25/// @brief A scope that crashes if coroutine context switches are attempted.
26///
27/// This is useful to prevent accidental waiting operations in places where they would be disastrous,
28/// e.g. while holding a thread-local resource.
29///
30/// The check only happens in debug builds.
32public:
33 CoroutineSwitchBanScope() noexcept;
34
35 CoroutineSwitchBanScope(CoroutineSwitchBanScope&&) = delete;
37 ~CoroutineSwitchBanScope();
38};
39
40/// @brief The scope that grants access to a thread-local variable.
41///
42/// @see compiler::ThreadLocal
43template <typename VariableType>
44class ThreadLocalScope final : private CoroutineSwitchBanScope {
45public:
46 ThreadLocalScope(ThreadLocalScope&&) = delete;
47 ThreadLocalScope& operator=(ThreadLocalScope&&) = delete;
48 ~ThreadLocalScope() = default;
49
50 /// Access the thread-local variable.
51 VariableType& operator*() & noexcept USERVER_IMPL_LIFETIME_BOUND;
52
53 /// Access the thread-local variable.
54 VariableType* operator->() & noexcept USERVER_IMPL_LIFETIME_BOUND;
55
56 /// @cond
57 explicit ThreadLocalScope(VariableType& variable) noexcept;
58
59 // Store ThreadLocalScope to a variable before using.
60 VariableType& operator*() && noexcept = delete;
61
62 // Store ThreadLocalScope to a variable before using.
63 VariableType* operator->() && noexcept = delete;
64 /// @endcond
65
66private:
67 static_assert(!std::is_reference_v<VariableType>);
68 static_assert(!std::is_const_v<VariableType>);
69
70 VariableType& variable_;
71};
72
73/// @brief Creates a unique thread-local variable that can be used
74/// in a coroutine-safe manner.
75///
76/// Thread-local variables are known to cause issues when used together
77/// with userver coroutines:
78///
79/// * https://github.com/userver-framework/userver/issues/235
80/// * https://github.com/userver-framework/userver/issues/242
81///
82/// Thread-local variables created through this class are protected against
83/// these issues.
84///
85/// `ThreadLocal` should be passed a factory function that constructs the variable. Example usage:
86///
87/// @snippet compiler/thread_local_test.cpp sample definition
88/// @snippet compiler/thread_local_test.cpp sample
89///
90/// An example with slightly more complex initialization for the variable:
91///
92/// @snippet compiler/thread_local_test.cpp sample factory
93///
94/// Once acquired through @a Use, the reference to the thread-local variable
95/// should not be returned or otherwise escape the scope
96/// of the ThreadLocalScope object. An example of buggy code:
97///
98/// @code
99/// compiler::ThreadLocal local_buffer = [] { return std::string{}; };
100///
101/// std::string_view PrepareBuffer(std::string_view x, std::string_view y) {
102/// const auto buffer = local_buffer.Use();
103/// buffer->clear();
104/// buffer->append(x);
105/// buffer->append(y);
106/// return *buffer; // <- BUG! Reference to thread-local escapes the scope
107/// }
108/// @endcode
109///
110/// Do not store a reference to the thread-local object in a separate variable:
111///
112/// @code
113/// compiler::ThreadLocal local_buffer = [] { return std::string{}; };
114///
115/// void WriteMessage(std::string_view x, std::string_view y) {
116/// auto buffer_scope = local_buffer.Use();
117/// // Code smell! This makes it more difficult to see that `buffer` is
118/// // a reference to thread-local at a glance.
119/// auto& buffer = *buffer_scope;
120/// buffer.clear();
121/// // ...
122/// }
123/// @endcode
124///
125/// Until the variable name goes out of scope, userver engine synchronization
126/// primitives and clients (web or db) should not be used.
127template <typename VariableType, typename Factory>
128class ThreadLocal final {
129 static_assert(std::is_empty_v<Factory>);
130 static_assert(std::is_same_v<VariableType, std::invoke_result_t<const Factory&>>);
131
132public:
133 consteval ThreadLocal()
134 : factory_(Factory{})
135 {}
136
137 consteval /*implicit*/ ThreadLocal(Factory factory)
138 : factory_(factory)
139 {}
140
141 ThreadLocalScope<VariableType> Use() { return ThreadLocalScope<VariableType>(impl::ThreadLocal(factory_)); }
142
143private:
144 // The ThreadLocal instance should have static storage duration. Still, if a
145 // user defines it as a local variable or even a thread_local variable, it
146 // should be harmless in practice, because ThreadLocal is an empty type,
147 // mainly used to store the `FactoryFunc` template parameter.
148 [[no_unique_address]] Factory factory_;
149};
150
151template <typename Factory>
153
154inline CoroutineSwitchBanScope::CoroutineSwitchBanScope() noexcept {
155#ifndef NDEBUG
156 impl::IncrementLocalCoroutineSwitchBans();
157#endif
158}
159
160inline CoroutineSwitchBanScope::~CoroutineSwitchBanScope() {
161#ifndef NDEBUG
162 impl::DecrementLocalCoroutineSwitchBans();
163#endif
164}
165
166template <typename VariableType>
167ThreadLocalScope<VariableType>::ThreadLocalScope(VariableType& variable) noexcept : variable_(variable) {}
168
169template <typename VariableType>
170VariableType& ThreadLocalScope<VariableType>::operator*() & noexcept USERVER_IMPL_LIFETIME_BOUND {
171 return variable_;
172}
173
174template <typename VariableType>
175VariableType* ThreadLocalScope<VariableType>::operator->() & noexcept USERVER_IMPL_LIFETIME_BOUND {
176 return &**this;
177}
178
179} // namespace compiler
180
181USERVER_NAMESPACE_END