[K/N] OnceComputable C++ utility class
This commit is contained in:
committed by
Space Team
parent
e650ba9855
commit
728e9a5811
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2010-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
#include <ManuallyScoped.hpp>
|
||||
#include <Utils.hpp>
|
||||
|
||||
namespace kotlin {
|
||||
|
||||
/** A value that can be computed only once. even with concurrent attempts from multiple threads. */
|
||||
template <typename T>
|
||||
class OnceComputable : private Pinned {
|
||||
public:
|
||||
~OnceComputable() noexcept { result_.destroy(); }
|
||||
|
||||
const T* tryGetValue() const noexcept {
|
||||
if (computed_.load(std::memory_order_acquire)) return &*result_;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
template <typename Computer>
|
||||
const T& ensureComputed(Computer&& computer) noexcept(noexcept(computer())) {
|
||||
if (auto res = tryGetValue()) {
|
||||
return *res;
|
||||
}
|
||||
|
||||
std::lock_guard lock(mutex_);
|
||||
if (!computed_.load(std::memory_order_relaxed)) {
|
||||
result_.construct(computer());
|
||||
computed_.store(true, std::memory_order_release);
|
||||
}
|
||||
return *result_;
|
||||
}
|
||||
|
||||
template <typename Computer>
|
||||
const T& operator=(Computer&& computer) noexcept(noexcept(computer())) {
|
||||
return ensureComputed(std::forward<Computer>(computer));
|
||||
}
|
||||
|
||||
private:
|
||||
ManuallyScoped<T> result_;
|
||||
|
||||
std::atomic<bool> computed_ = false;
|
||||
std::mutex mutex_;
|
||||
};
|
||||
|
||||
template <>
|
||||
class OnceComputable<void> : private Pinned {
|
||||
public:
|
||||
bool computed() const noexcept { return computed_.load(std::memory_order_acquire); }
|
||||
|
||||
template <typename Computer>
|
||||
void ensureComputed(Computer&& computer) noexcept(noexcept(computer())) {
|
||||
if (computed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock(mutex_);
|
||||
if (!computed_.load(std::memory_order_relaxed)) {
|
||||
computer();
|
||||
computed_.store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Computer>
|
||||
void operator=(Computer&& computer) noexcept(noexcept(computer())) {
|
||||
return ensureComputed(std::forward<Computer>(computer));
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<bool> computed_ = false;
|
||||
std::mutex mutex_;
|
||||
};
|
||||
|
||||
/** A guard helping to ensure a certain action is executed only once. Even with concurrent attempts from multiple threads. */
|
||||
class OnceExecutable : private OnceComputable<void> {
|
||||
public:
|
||||
bool executed() const noexcept { return computed(); }
|
||||
|
||||
template <typename Action>
|
||||
void ensureExecuted(Action&& action) noexcept(noexcept(action())) {
|
||||
ensureComputed(std::forward<Action>(action));
|
||||
}
|
||||
|
||||
template <typename Action>
|
||||
void operator=(Action&& action) noexcept(noexcept(action())) {
|
||||
ensureExecuted(std::forward<Action>(action));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace kotlin
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2010-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "concurrent/Once.hpp"
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "ScopedThread.hpp"
|
||||
#include "TestSupport.hpp"
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
template <typename T>
|
||||
struct OnceTest : public ::testing::Test {
|
||||
testing::StrictMock<testing::MockFunction<T()>> mockComputer_;
|
||||
};
|
||||
|
||||
template <int kSize>
|
||||
struct LargeValue {
|
||||
LargeValue() {
|
||||
for (int i = 0; i < kSize; ++i) {
|
||||
data_[i] = i;
|
||||
}
|
||||
}
|
||||
LargeValue(const LargeValue& that) {
|
||||
for (int i = 0; i < kSize; ++i) {
|
||||
data_[i] = that.data_[i];
|
||||
}
|
||||
}
|
||||
bool operator==(const LargeValue& other) const {
|
||||
for (int i = 0; i < kSize; ++i) {
|
||||
if (data_[i] != other.data_[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::array<int, kSize> data_;
|
||||
};
|
||||
|
||||
using ValueTypes = testing::Types<int, LargeValue<1000>>;
|
||||
TYPED_TEST_SUITE(OnceTest, ValueTypes);
|
||||
|
||||
TYPED_TEST(OnceTest, SingleThread) {
|
||||
using T = TypeParam;
|
||||
|
||||
OnceComputable<T> onceComputable;
|
||||
EXPECT_THAT(onceComputable.tryGetValue(), nullptr);
|
||||
|
||||
EXPECT_CALL(this->mockComputer_, Call()).WillOnce(testing::Return(T{}));
|
||||
|
||||
onceComputable = [this] { return this->mockComputer_.Call(); };
|
||||
onceComputable = [this] { return this->mockComputer_.Call(); };
|
||||
|
||||
testing::Mock::VerifyAndClearExpectations(&this->mockComputer_);
|
||||
|
||||
EXPECT_THAT(*onceComputable.tryGetValue(), T{});
|
||||
}
|
||||
|
||||
TYPED_TEST(OnceTest, Concurrent) {
|
||||
using T = TypeParam;
|
||||
|
||||
EXPECT_CALL(this->mockComputer_, Call()).WillOnce(testing::Return(T{}));
|
||||
|
||||
OnceComputable<T> onceComputable;
|
||||
EXPECT_THAT(onceComputable.tryGetValue(), nullptr);
|
||||
|
||||
std::atomic ready = false;
|
||||
std::vector<ScopedThread> threads;
|
||||
for (int i = 0; i < kDefaultThreadCount; ++i) {
|
||||
threads.emplace_back([&]() noexcept {
|
||||
while (!ready) std::this_thread::yield();
|
||||
onceComputable = [this] { return this->mockComputer_.Call(); };
|
||||
});
|
||||
}
|
||||
|
||||
ready = true;
|
||||
threads.clear();
|
||||
|
||||
testing::Mock::VerifyAndClearExpectations(&this->mockComputer_);
|
||||
|
||||
EXPECT_THAT(*onceComputable.tryGetValue(), T{});
|
||||
}
|
||||
Reference in New Issue
Block a user