[K/N] OnceComputable C++ utility class

This commit is contained in:
Aleksei.Glushko
2024-01-31 13:54:21 +01:00
committed by Space Team
parent e650ba9855
commit 728e9a5811
2 changed files with 180 additions and 0 deletions
@@ -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{});
}