diff --git a/util/BUILD.gn b/util/BUILD.gn index 9e58bf32..351c0cdd 100644 --- a/util/BUILD.gn +++ b/util/BUILD.gn @@ -267,6 +267,7 @@ crashpad_static_library("util") { "stream/zlib_output_stream.h", "string/split_string.cc", "string/split_string.h", + "synchronization/scoped_spin_guard.h", "synchronization/semaphore.h", "thread/stoppable.h", "thread/thread.cc", @@ -775,6 +776,7 @@ source_set("util_test") { "stream/test_output_stream.h", "stream/zlib_output_stream_test.cc", "string/split_string_test.cc", + "synchronization/scoped_spin_guard_test.cc", "synchronization/semaphore_test.cc", "thread/thread_log_messages_test.cc", "thread/thread_test.cc", diff --git a/util/synchronization/scoped_spin_guard.h b/util/synchronization/scoped_spin_guard.h new file mode 100644 index 00000000..f924d991 --- /dev/null +++ b/util/synchronization/scoped_spin_guard.h @@ -0,0 +1,122 @@ +// Copyright 2022 The Crashpad Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef CRASHPAD_UTIL_SYNCHRONIZATION_SCOPED_SPIN_GUARD_H_ +#define CRASHPAD_UTIL_SYNCHRONIZATION_SCOPED_SPIN_GUARD_H_ + +#include +#include +#include + +#include "base/check.h" +#include "base/notreached.h" +#include "util/misc/clock.h" + +namespace crashpad { + +//! \brief Spinlock state for `ScopedSpinGuard`. +struct SpinGuardState final { + //! \brief A `ScopedSpinGuard` in an unlocked state. + constexpr SpinGuardState() : locked(false) {} + + SpinGuardState(const SpinGuardState&) = delete; + SpinGuardState& operator=(const SpinGuardState&) = delete; + + //! \brief `true` if the `ScopedSpinGuard` is locked, `false` otherwise. + std::atomic locked; + static_assert(std::atomic::is_always_lock_free, + "std::atomic may not be signal-safe"); + static_assert(sizeof(std::atomic) == sizeof(bool), + "std::atomic adds size to bool"); +}; + +//! \brief A scoped mutual-exclusion guard wrapping a `SpinGuardState` with RAII +//! semantics. +class ScopedSpinGuard final { + //! \brief The duration in nanoseconds between attempts to lock the spinlock. + static constexpr uint64_t kSpinGuardSleepTimeNanos = 10; + + public: + ScopedSpinGuard(const ScopedSpinGuard&) = delete; + ScopedSpinGuard& operator=(const ScopedSpinGuard&) = delete; + ScopedSpinGuard(ScopedSpinGuard&& other) noexcept : state_(nullptr) { + std::swap(state_, other.state_); + } + ScopedSpinGuard& operator=(ScopedSpinGuard&& other) { + std::swap(state_, other.state_); + return *this; + } + + //! \brief Spins up to `timeout_nanos` nanoseconds trying to lock `state`. + //! \param[in] timeout_nanos The timeout in nanoseconds after which this gives + //! up trying to lock the spinlock and returns `std::nullopt`. + //! \param[in,out] state The spinlock state to attempt to lock. This method + //! holds a pointer to `state`, so `state` must outlive the lifetime of + //! this object. + //! \return The locked `ScopedSpinGuard` on success, or `std::nullopt` on + //! timeout. + static std::optional TryCreateScopedSpinGuard( + uint64_t timeout_nanos, + SpinGuardState& state) { + const uint64_t clock_end_time_nanos = + ClockMonotonicNanoseconds() + timeout_nanos; + while (true) { + bool expected_current_value = false; + if (state.locked.compare_exchange_weak(expected_current_value, + true, + std::memory_order_acquire, + std::memory_order_relaxed)) { + return std::make_optional(state); + } + if (ClockMonotonicNanoseconds() >= clock_end_time_nanos) { + return std::nullopt; + } + SleepNanoseconds(kSpinGuardSleepTimeNanos); + } + + NOTREACHED(); + } + + ~ScopedSpinGuard() { + if (state_) { +#ifdef NDEBUG + state_->locked.store(false, std::memory_order_release); +#else + bool old = state_->locked.exchange(false, std::memory_order_release); + DCHECK(old); +#endif + } + } + + //! \brief A `ScopedSpinGuard` wrapping a locked `SpinGuardState`. + //! \param[in,out] locked_state A locked `SpinGuardState`. This method + //! holds a pointer to `state`, so `state` must outlive the lifetime of + //! this object. + ScopedSpinGuard(SpinGuardState& locked_state) : state_(&locked_state) { + DCHECK(locked_state.locked); + } + + private: + // \brief Optional spinlock state, unlocked when this object goes out of + // scope. + // + // If this is `nullptr`, then this object has been moved from, and the state + // is no longer valid. In that case, nothing will be unlocked when this object + // is destroyed. + SpinGuardState* state_; +}; + +} // namespace crashpad + +#endif // CRASHPAD_UTIL_SYNCHRONIZATION_SCOPED_SPIN_GUARD_H_ diff --git a/util/synchronization/scoped_spin_guard_test.cc b/util/synchronization/scoped_spin_guard_test.cc new file mode 100644 index 00000000..9980d50d --- /dev/null +++ b/util/synchronization/scoped_spin_guard_test.cc @@ -0,0 +1,109 @@ +// Copyright 2022 The Crashpad Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/synchronization/scoped_spin_guard.h" + +#include +#include + +#include "gtest/gtest.h" +#include "util/misc/clock.h" + +namespace crashpad { +namespace test { +namespace { + +TEST(ScopedSpinGuard, TryCreateScopedSpinGuardShouldLockStateWhileInScope) { + SpinGuardState s; + EXPECT_FALSE(s.locked); + { + std::optional guard = + ScopedSpinGuard::TryCreateScopedSpinGuard(/*timeout_nanos=*/0, s); + EXPECT_NE(std::nullopt, guard); + EXPECT_TRUE(s.locked); + } + EXPECT_FALSE(s.locked); +} + +TEST( + ScopedSpinGuard, + TryCreateScopedSpinGuardWithZeroTimeoutShouldFailImmediatelyIfStateLocked) { + SpinGuardState s; + s.locked = true; + std::optional guard = + ScopedSpinGuard::TryCreateScopedSpinGuard(/*timeout_nanos=*/0, s); + EXPECT_EQ(std::nullopt, guard); + EXPECT_TRUE(s.locked); +} + +TEST( + ScopedSpinGuard, + TryCreateScopedSpinGuardWithNonZeroTimeoutShouldSucceedIfStateUnlockedDuringTimeout) { + SpinGuardState s; + s.locked = true; + std::thread unlock_thread([&s] { + constexpr uint64_t kUnlockThreadSleepTimeNanos = 10000; // 10 us + EXPECT_TRUE(s.locked); + SleepNanoseconds(kUnlockThreadSleepTimeNanos); + s.locked = false; + }); + constexpr uint64_t kLockThreadTimeoutNanos = 180000000000ULL; // 3 minutes + std::optional guard = + ScopedSpinGuard::TryCreateScopedSpinGuard(kLockThreadTimeoutNanos, s); + EXPECT_NE(std::nullopt, guard); + EXPECT_TRUE(s.locked); + unlock_thread.join(); +} + +TEST(ScopedSpinGuard, SwapShouldMaintainSpinLock) { + SpinGuardState s; + std::optional outer_guard; + EXPECT_EQ(std::nullopt, outer_guard); + { + std::optional inner_guard = + ScopedSpinGuard::TryCreateScopedSpinGuard(/*timeout_nanos=*/0, s); + EXPECT_NE(std::nullopt, inner_guard); + EXPECT_TRUE(s.locked); + // If the move-assignment operator for `ScopedSpinGuard` is implemented + // incorrectly (e.g., the `= default` implementation), `inner_guard` + // will incorrectly think it still "owns" the spinlock after the swap, + // and when it falls out of scope, it will release the lock prematurely + // when it destructs. + // + // Confirm that the spinlock stays locked even after the swap. + std::swap(outer_guard, inner_guard); + EXPECT_TRUE(s.locked); + EXPECT_EQ(std::nullopt, inner_guard); + } + EXPECT_NE(std::nullopt, outer_guard); + EXPECT_TRUE(s.locked); +} + +TEST(ScopedSpinGuard, MoveAssignmentShouldMaintainSpinLock) { + SpinGuardState s; + std::optional outer_guard; + EXPECT_EQ(std::nullopt, outer_guard); + { + outer_guard = + ScopedSpinGuard::TryCreateScopedSpinGuard(/*timeout_nanos=*/0, s); + EXPECT_NE(std::nullopt, outer_guard); + EXPECT_TRUE(s.locked); + } + EXPECT_NE(std::nullopt, outer_guard); + EXPECT_TRUE(s.locked); +} + +} // namespace +} // namespace test +} // namespace crashpad