Skip to content

Commit 26ef66e

Browse files
committed
CpuBoundWork#CpuBoundWork(): don't spin on atomic int to acquire slot
This is inefficient and involves unfair scheduling. The latter implies possible bad surprises regarding waiting durations on busy nodes. Instead, use AsioConditionVariable#Wait() if there are no free slots. It's notified by others' CpuBoundWork#~CpuBoundWork() once finished.
1 parent b413287 commit 26ef66e

File tree

2 files changed

+78
-30
lines changed

2 files changed

+78
-30
lines changed

lib/base/io-engine.cpp

+41-10
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,55 @@
1616

1717
using namespace icinga;
1818

19-
CpuBoundWork::CpuBoundWork(boost::asio::yield_context yc, boost::asio::io_context::strand&)
19+
CpuBoundWork::CpuBoundWork(boost::asio::yield_context yc, boost::asio::io_context::strand& strand)
2020
: m_Done(false)
2121
{
2222
auto& ioEngine (IoEngine::Get());
23+
auto& sem (ioEngine.m_CpuBoundSemaphore);
24+
std::unique_lock<std::mutex> lock (sem.Mutex);
2325

24-
for (;;) {
25-
auto availableSlots (ioEngine.m_CpuBoundSemaphore.fetch_sub(1));
26+
if (sem.FreeSlots) {
27+
--sem.FreeSlots;
28+
return;
29+
}
30+
31+
auto cv (Shared<AsioConditionVariable>::Make(ioEngine.GetIoContext()));
32+
bool gotSlot = false;
33+
auto pos (sem.Waiting.insert(sem.Waiting.end(), IoEngine::CpuBoundQueueItem{&strand, cv, &gotSlot}));
34+
35+
lock.unlock();
36+
37+
try {
38+
cv->Wait(yc);
39+
} catch (...) {
40+
std::unique_lock<std::mutex> lock (sem.Mutex);
2641

27-
if (availableSlots < 1) {
28-
ioEngine.m_CpuBoundSemaphore.fetch_add(1);
29-
IoEngine::YieldCurrentCoroutine(yc);
30-
continue;
42+
if (gotSlot) {
43+
lock.unlock();
44+
Done();
45+
} else {
46+
sem.Waiting.erase(pos);
3147
}
3248

33-
break;
49+
throw;
3450
}
3551
}
3652

3753
void CpuBoundWork::Done()
3854
{
3955
if (!m_Done) {
40-
IoEngine::Get().m_CpuBoundSemaphore.fetch_add(1);
56+
auto& sem (IoEngine::Get().m_CpuBoundSemaphore);
57+
std::unique_lock<std::mutex> lock (sem.Mutex);
58+
59+
if (sem.Waiting.empty()) {
60+
++sem.FreeSlots;
61+
} else {
62+
auto next (sem.Waiting.front());
63+
64+
*next.GotSlot = true;
65+
sem.Waiting.pop_front();
66+
boost::asio::post(*next.Strand, [cv = std::move(next.CV)]() { cv->Set(); });
67+
}
4168

4269
m_Done = true;
4370
}
@@ -58,7 +85,11 @@ boost::asio::io_context& IoEngine::GetIoContext()
5885
IoEngine::IoEngine() : m_IoContext(), m_KeepAlive(boost::asio::make_work_guard(m_IoContext)), m_Threads(decltype(m_Threads)::size_type(Configuration::Concurrency * 2u)), m_AlreadyExpiredTimer(m_IoContext)
5986
{
6087
m_AlreadyExpiredTimer.expires_at(boost::posix_time::neg_infin);
61-
m_CpuBoundSemaphore.store(Configuration::Concurrency * 3u / 2u);
88+
89+
{
90+
std::unique_lock<std::mutex> lock (m_CpuBoundSemaphore.Mutex);
91+
m_CpuBoundSemaphore.FreeSlots = Configuration::Concurrency * 3u / 2u;
92+
}
6293

6394
for (auto& thread : m_Threads) {
6495
thread = std::thread(&IoEngine::RunEventLoop, this);

lib/base/io-engine.hpp

+37-20
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
#include "base/exception.hpp"
77
#include "base/lazy-init.hpp"
88
#include "base/logger.hpp"
9+
#include "base/shared.hpp"
910
#include "base/shared-object.hpp"
1011
#include <atomic>
12+
#include <cstdint>
1113
#include <exception>
14+
#include <list>
1215
#include <memory>
16+
#include <mutex>
1317
#include <thread>
1418
#include <utility>
1519
#include <vector>
@@ -31,7 +35,7 @@ namespace icinga
3135
class CpuBoundWork
3236
{
3337
public:
34-
CpuBoundWork(boost::asio::yield_context yc, boost::asio::io_context::strand&);
38+
CpuBoundWork(boost::asio::yield_context yc, boost::asio::io_context::strand& strand);
3539
CpuBoundWork(const CpuBoundWork&) = delete;
3640
CpuBoundWork(CpuBoundWork&&) = delete;
3741
CpuBoundWork& operator=(const CpuBoundWork&) = delete;
@@ -48,6 +52,25 @@ class CpuBoundWork
4852
bool m_Done;
4953
};
5054

55+
56+
/**
57+
* Condition variable which doesn't block I/O threads
58+
*
59+
* @ingroup base
60+
*/
61+
class AsioConditionVariable
62+
{
63+
public:
64+
AsioConditionVariable(boost::asio::io_context& io, bool init = false);
65+
66+
void Set();
67+
void Clear();
68+
void Wait(boost::asio::yield_context yc);
69+
70+
private:
71+
boost::asio::deadline_timer m_Timer;
72+
};
73+
5174
/**
5275
* Async I/O engine
5376
*
@@ -110,6 +133,13 @@ class IoEngine
110133
}
111134

112135
private:
136+
struct CpuBoundQueueItem
137+
{
138+
boost::asio::io_context::strand* Strand;
139+
Shared<AsioConditionVariable>::Ptr CV;
140+
bool* GotSlot;
141+
};
142+
113143
IoEngine();
114144

115145
void RunEventLoop();
@@ -120,29 +150,16 @@ class IoEngine
120150
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> m_KeepAlive;
121151
std::vector<std::thread> m_Threads;
122152
boost::asio::deadline_timer m_AlreadyExpiredTimer;
123-
std::atomic_int_fast32_t m_CpuBoundSemaphore;
124-
};
125153

126-
class TerminateIoThread : public std::exception
127-
{
154+
struct {
155+
std::mutex Mutex;
156+
uint_fast32_t FreeSlots;
157+
std::list<CpuBoundQueueItem> Waiting;
158+
} m_CpuBoundSemaphore;
128159
};
129160

130-
/**
131-
* Condition variable which doesn't block I/O threads
132-
*
133-
* @ingroup base
134-
*/
135-
class AsioConditionVariable
161+
class TerminateIoThread : public std::exception
136162
{
137-
public:
138-
AsioConditionVariable(boost::asio::io_context& io, bool init = false);
139-
140-
void Set();
141-
void Clear();
142-
void Wait(boost::asio::yield_context yc);
143-
144-
private:
145-
boost::asio::deadline_timer m_Timer;
146163
};
147164

148165
/**

0 commit comments

Comments
 (0)