-
Notifications
You must be signed in to change notification settings - Fork 589
CpuBoundWork#CpuBoundWork(): don't spin on atomic int to acquire slot #9990
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
3d5e031
eb4e601
9018385
659bb9e
1e24adf
cb18ed0
c88502b
e4b73f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -16,60 +16,121 @@ | |||||
|
||||||
using namespace icinga; | ||||||
|
||||||
CpuBoundWork::CpuBoundWork(boost::asio::yield_context yc) | ||||||
/** | ||||||
* 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, boost::asio::io_context::strand& strand) | ||||||
: m_Done(false) | ||||||
{ | ||||||
auto& ioEngine (IoEngine::Get()); | ||||||
VERIFY(strand.running_in_this_thread()); | ||||||
|
||||||
for (;;) { | ||||||
auto availableSlots (ioEngine.m_CpuBoundSemaphore.fetch_sub(1)); | ||||||
auto& ie (IoEngine::Get()); | ||||||
Shared<AsioConditionVariable>::Ptr cv; | ||||||
|
||||||
while (!TryAcquireSlot()) { | ||||||
if (!cv) { | ||||||
cv = Shared<AsioConditionVariable>::Make(ie.GetIoContext()); | ||||||
|
||||||
if (availableSlots < 1) { | ||||||
ioEngine.m_CpuBoundSemaphore.fetch_add(1); | ||||||
IoEngine::YieldCurrentCoroutine(yc); | ||||||
continue; | ||||||
// The above line may take a little bit, so let's optimistically re-check | ||||||
if (TryAcquireSlot()) { | ||||||
break; | ||||||
} | ||||||
} | ||||||
|
||||||
break; | ||||||
{ | ||||||
std::unique_lock lock (ie.m_CpuBoundWaitingMutex); | ||||||
|
||||||
// The above line may take even longer, so let's check again. | ||||||
// 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); | ||||||
} | ||||||
} | ||||||
|
||||||
CpuBoundWork::~CpuBoundWork() | ||||||
/** | ||||||
* 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() | ||||||
{ | ||||||
if (!m_Done) { | ||||||
IoEngine::Get().m_CpuBoundSemaphore.fetch_add(1); | ||||||
auto& ie (IoEngine::Get()); | ||||||
auto freeSlots (ie.m_CpuBoundSemaphore.load()); | ||||||
|
||||||
while (freeSlots > 0) { | ||||||
// 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 - 1)) { | ||||||
// ... 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) { | ||||||
IoEngine::Get().m_CpuBoundSemaphore.fetch_add(1); | ||||||
|
||||||
m_Done = true; | ||||||
} | ||||||
} | ||||||
|
||||||
IoBoundWorkSlot::IoBoundWorkSlot(boost::asio::yield_context yc) | ||||||
: yc(yc) | ||||||
{ | ||||||
IoEngine::Get().m_CpuBoundSemaphore.fetch_add(1); | ||||||
} | ||||||
|
||||||
IoBoundWorkSlot::~IoBoundWorkSlot() | ||||||
{ | ||||||
auto& ioEngine (IoEngine::Get()); | ||||||
|
||||||
for (;;) { | ||||||
auto availableSlots (ioEngine.m_CpuBoundSemaphore.fetch_sub(1)); | ||||||
|
||||||
if (availableSlots < 1) { | ||||||
ioEngine.m_CpuBoundSemaphore.fetch_add(1); | ||||||
IoEngine::YieldCurrentCoroutine(yc); | ||||||
continue; | ||||||
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) < 1) { | ||||||
// 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& [strand, cv] : subscribers) { | ||||||
boost::asio::post(strand, [cv = std::move(cv)] { cv->NotifyOne(); }); | ||||||
} | ||||||
} | ||||||
|
||||||
break; | ||||||
} | ||||||
} | ||||||
|
||||||
|
@@ -85,9 +146,8 @@ 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_AlreadyExpiredTimer(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_AlreadyExpiredTimer.expires_at(boost::posix_time::neg_infin); | ||||||
m_CpuBoundSemaphore.store(Configuration::Concurrency * 3u / 2u); | ||||||
|
||||||
for (auto& thread : m_Threads) { | ||||||
|
@@ -173,6 +233,30 @@ 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(boost::posix_time::pos_infin); | ||||||
} | ||||||
|
||||||
void AsioConditionVariable::Wait(boost::asio::yield_context yc) | ||||||
{ | ||||||
boost::system::error_code ec; | ||||||
m_Timer.async_wait(yc[ec]); | ||||||
} | ||||||
|
||||||
bool AsioConditionVariable::NotifyOne() | ||||||
{ | ||||||
boost::system::error_code ec; | ||||||
return m_Timer.cancel_one(ec); | ||||||
} | ||||||
|
||||||
size_t AsioConditionVariable::NotifyAll() | ||||||
{ | ||||||
boost::system::error_code ec; | ||||||
return m_Timer.cancel(ec); | ||||||
} | ||||||
Comment on lines
+248
to
+258
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the deal with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Well, the one is (now) an actual CV (and complies to its name). The other is like Python's threading.Event – an awaitable bool.
I know I'm not using NotifyAll(). I just fully implemented the new AsioConditionVariable class.
I could also use AsioEvent#Set(). But then I eventually had to call AsioEvent#Clear() somewhere, resetting the timer to where it was before. What for? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. E.g: icinga2/lib/remote/jsonrpcconnection.cpp Lines 144 to 145 in b044f39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You don't have to if you don't want to reuse the same instance elsewhere. Your argument for introducing yet another duplicate applies to your new class as well. As far as I know, you can't just call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Source... ?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Once an AsioEvent has been Set(), Wait() returns instantly until the next Clear(). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Well, my source was something like that, and I didn't get the BOOST_AUTO_TEST_CASE(test_test)
{
boost::asio::io_context io;
boost::asio::deadline_timer timer(io);
timer.expires_from_now(boost::posix_time::seconds(5));
timer.async_wait([&timer](const boost::system::error_code& ec) {
BOOST_TEST(ec == boost::system::errc::operation_canceled);
});
boost::asio::post(io, [&timer]() {
timer.cancel();
timer.async_wait([](boost::system::error_code ec) {
std::cout << "ec.message()" << "\n";
});
});
io.run();
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That Heisenbug doesn't speak for the test's reliability. Mine, in contrast, confirms that one CAN wait for the timer after being cancelled: BOOST_AUTO_TEST_CASE(test_test)
{
boost::asio::io_context io;
boost::asio::deadline_timer timer(io);
// OK yet, will change once lambda below called
boost::system::error_code ec1;
// !OK yet, will change once lambda below called
boost::system::error_code ec2;
ec2.assign(boost::system::errc::operation_canceled, boost::system::generic_category());
BOOST_CHECK_NE(ec2, boost::system::error_code());
timer.expires_from_now(boost::posix_time::seconds(5));
timer.async_wait([&ec1](const boost::system::error_code& ec) {
ec1 = ec; // (2)
});
boost::asio::post(io, [&timer, &ec2] {
timer.cancel(); // (1)
timer.async_wait([&ec2](boost::system::error_code ec) {
ec2 = ec; // (3), after 5s
});
});
io.run();
// Free-standing checks, so that not-called lambda doesn't fake an OK
BOOST_CHECK_EQUAL(ec1, boost::system::errc::operation_canceled);
BOOST_CHECK_EQUAL(ec2, boost::system::error_code());
} |
||||||
|
||||||
/** | ||||||
* Cancels any pending timeout callback. | ||||||
* | ||||||
|
Uh oh!
There was an error while loading. Please reload this page.