diff --git a/kotlin-native/runtime/src/main/cpp/concurrent/Once.hpp b/kotlin-native/runtime/src/main/cpp/concurrent/Once.hpp new file mode 100644 index 00000000000..80f1f20b87d --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/concurrent/Once.hpp @@ -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 +#include + +#include +#include + +namespace kotlin { + +/** A value that can be computed only once. even with concurrent attempts from multiple threads. */ +template +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 + 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 + const T& operator=(Computer&& computer) noexcept(noexcept(computer())) { + return ensureComputed(std::forward(computer)); + } + +private: + ManuallyScoped result_; + + std::atomic computed_ = false; + std::mutex mutex_; +}; + +template <> +class OnceComputable : private Pinned { +public: + bool computed() const noexcept { return computed_.load(std::memory_order_acquire); } + + template + 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 + void operator=(Computer&& computer) noexcept(noexcept(computer())) { + return ensureComputed(std::forward(computer)); + } + +private: + std::atomic 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 { +public: + bool executed() const noexcept { return computed(); } + + template + void ensureExecuted(Action&& action) noexcept(noexcept(action())) { + ensureComputed(std::forward(action)); + } + + template + void operator=(Action&& action) noexcept(noexcept(action())) { + ensureExecuted(std::forward(action)); + } +}; + +} // namespace kotlin diff --git a/kotlin-native/runtime/src/main/cpp/concurrent/OnceTest.cpp b/kotlin-native/runtime/src/main/cpp/concurrent/OnceTest.cpp new file mode 100644 index 00000000000..28007434c32 --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/concurrent/OnceTest.cpp @@ -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 +struct OnceTest : public ::testing::Test { + testing::StrictMock> mockComputer_; +}; + +template +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 data_; +}; + +using ValueTypes = testing::Types>; +TYPED_TEST_SUITE(OnceTest, ValueTypes); + +TYPED_TEST(OnceTest, SingleThread) { + using T = TypeParam; + + OnceComputable 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 onceComputable; + EXPECT_THAT(onceComputable.tryGetValue(), nullptr); + + std::atomic ready = false; + std::vector 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{}); +}