-
Notifications
You must be signed in to change notification settings - Fork 612
Expand file tree
/
Copy pathio-engine.cpp
More file actions
263 lines (221 loc) · 7.01 KB
/
Copy pathio-engine.cpp
File metadata and controls
263 lines (221 loc) · 7.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
// SPDX-FileCopyrightText: 2012 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "base/configuration.hpp"
#include "base/exception.hpp"
#include "base/io-engine.hpp"
#include "base/lazy-init.hpp"
#include "base/logger.hpp"
#include <exception>
#include <memory>
#include <thread>
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/post.hpp>
#include <boost/date_time/posix_time/ptime.hpp>
#include <boost/system/error_code.hpp>
using namespace icinga;
/**
* Acquires a slot for CPU-bound work.
*
* If and as long as the lock-free TryAcquireSlot() doesn't succeed,
* subscribes to the slow path by waiting on a condition variable.
* It is woken up by Done() which is called by the destructor.
*
* @param yc Needed to asynchronously wait for the condition variable.
* @param strand Where to post the wake-up of the condition variable.
*/
CpuBoundWork::CpuBoundWork(boost::asio::yield_context yc, IoStrand& strand)
: m_Done(false)
{
VERIFY(IoEngine::IsStrandRunningOnThisThread(strand));
auto& ie (IoEngine::Get());
Shared<AsioConditionVariable>::Ptr cv;
while (!TryAcquireSlot()) {
if (!cv) {
cv = Shared<AsioConditionVariable>::Make(ie.GetIoContext());
}
{
std::unique_lock lock (ie.m_CpuBoundWaitingMutex);
// The above lines may take a little bit, so let's optimistically re-check.
// Also mitigate lost wake-ups by re-checking during the lock:
//
// During our lock, Done() can't retrieve the subscribers to wake up,
// so any ongoing wake-up is either done at this point or has not started yet.
// If such a wake-up is done, it's a lost wake-up to us unless we re-check here
// whether the slot being freed (just before the wake-up) is still available.
if (TryAcquireSlot()) {
break;
}
// If the (hypothetical) slot mentioned above was taken by another coroutine,
// there are no free slots again, just as if no wake-ups happened just now.
ie.m_CpuBoundWaiting.emplace_back(strand, cv);
}
cv->Wait(yc);
}
}
/**
* Tries to acquire a slot for CPU-bound work.
*
* Specifically, decrements the number of free slots (semaphore) by one,
* but only if it's currently greater than zero.
* Not falling below zero requires an atomic#compare_exchange_weak() loop
* instead of a simple atomic#fetch_sub() call, but it's also atomic.
*
* @return Whether a slot was acquired.
*/
bool CpuBoundWork::TryAcquireSlot()
{
auto& ie (IoEngine::Get());
auto freeSlots (ie.m_CpuBoundSemaphore.load());
while (freeSlots > 0u) {
// If ie.m_CpuBoundSemaphore was changed after the last load,
// compare_exchange_weak() will load its latest value into freeSlots for us to retry until...
if (ie.m_CpuBoundSemaphore.compare_exchange_weak(freeSlots, freeSlots - 1u)) {
// ... either we successfully decrement ie.m_CpuBoundSemaphore by one, ...
return true;
}
}
// ... or it becomes zero due to another coroutine.
return false;
}
/**
* Releases the own slot acquired by the constructor (TryAcquireSlot()) if not already done.
*
* Precisely, increments the number of free slots (semaphore) by one.
* Also wakes up all waiting constructors (slow path) if necessary.
*/
void CpuBoundWork::Done()
{
if (!m_Done) {
m_Done = true;
auto& ie (IoEngine::Get());
// The constructor takes the slow path only if the semaphore is full,
// so we only have to wake up constructors if the semaphore was full.
// This works because after fetch_add(), TryAcquireSlot() (fast path) will succeed.
if (ie.m_CpuBoundSemaphore.fetch_add(1) == 0u) {
// So now there are only slow path subscribers from just before the fetch_add() to be woken up.
// Precisely, only subscribers from just before the fetch_add() which turned 0 to 1.
decltype(ie.m_CpuBoundWaiting) subscribers;
{
// Locking after fetch_add() is safe because a delayed wake-up is fine.
// Wake-up of constructors which subscribed after the fetch_add() is also not a problem.
// In worst case, they will just re-subscribe to the slow path.
// Lost wake-ups are mitigated by the constructor, see its implementation comments.
std::unique_lock lock (ie.m_CpuBoundWaitingMutex);
std::swap(subscribers, ie.m_CpuBoundWaiting);
}
// Again, a delayed wake-up is fine, hence unlocked.
for (auto& subscriber : subscribers) {
boost::asio::post(subscriber.first, [cv = std::move(subscriber.second)] { cv->NotifyOne(); });
}
}
}
}
LazyInit<std::unique_ptr<IoEngine>> IoEngine::m_Instance ([]() { return std::unique_ptr<IoEngine>(new IoEngine()); });
IoEngine& IoEngine::Get()
{
return *m_Instance.Get();
}
boost::asio::io_context& IoEngine::GetIoContext()
{
return m_IoContext;
}
IoEngine::IoEngine() : m_IoContext(), m_KeepAlive(boost::asio::make_work_guard(m_IoContext)), m_Threads(decltype(m_Threads)::size_type(Configuration::Concurrency * 2u))
{
m_CpuBoundSemaphore.store(Configuration::Concurrency * 3u / 2u);
for (auto& thread : m_Threads) {
thread = std::thread(&IoEngine::RunEventLoop, this);
}
}
IoEngine::~IoEngine()
{
for ([[maybe_unused]] auto& thread : m_Threads) {
boost::asio::post(m_IoContext, []() {
throw TerminateIoThread();
});
thread.join();
}
}
void IoEngine::RunEventLoop()
{
for (;;) {
try {
m_IoContext.run();
break;
} catch (const TerminateIoThread&) {
break;
} catch (const std::exception& e) {
Log(LogCritical, "IoEngine", "Exception during I/O operation!");
Log(LogDebug, "IoEngine") << "Exception during I/O operation: " << DiagnosticInformation(e);
}
}
}
AsioEvent::AsioEvent(boost::asio::io_context& io, bool init)
: m_Timer(io)
{
m_Timer.expires_at(init ? boost::posix_time::neg_infin : boost::posix_time::pos_infin);
}
void AsioEvent::Set()
{
m_Timer.expires_at(boost::posix_time::neg_infin);
}
void AsioEvent::Clear()
{
m_Timer.expires_at(boost::posix_time::pos_infin);
}
void AsioEvent::Wait(boost::asio::yield_context yc)
{
boost::system::error_code ec;
m_Timer.async_wait(yc[ec]);
}
AsioDualEvent::AsioDualEvent(boost::asio::io_context& io, bool init)
: m_IsTrue(io, init), m_IsFalse(io, !init)
{
}
void AsioDualEvent::Set()
{
m_IsTrue.Set();
m_IsFalse.Clear();
}
void AsioDualEvent::Clear()
{
m_IsTrue.Clear();
m_IsFalse.Set();
}
void AsioDualEvent::WaitForSet(boost::asio::yield_context yc)
{
m_IsTrue.Wait(std::move(yc));
}
void AsioDualEvent::WaitForClear(boost::asio::yield_context yc)
{
m_IsFalse.Wait(std::move(yc));
}
AsioConditionVariable::AsioConditionVariable(boost::asio::io_context& io)
: m_Timer(io)
{
m_Timer.expires_at(decltype(m_Timer)::clock_type::time_point::max());
}
void AsioConditionVariable::Wait(boost::asio::yield_context yc)
{
boost::system::error_code ec;
m_Timer.async_wait(yc[ec]);
}
bool AsioConditionVariable::NotifyOne()
{
return m_Timer.cancel_one();
}
size_t AsioConditionVariable::NotifyAll()
{
return m_Timer.cancel();
}
/**
* Cancels any pending timeout callback.
*
* Must be called in the strand in which the callback was scheduled!
*/
void Timeout::Cancel()
{
m_Cancelled->store(true);
boost::system::error_code ec;
m_Timer.cancel(ec);
}