[orc-rt] Add LockedAccess utility. (#186737)
LockedAccess provides pointer-like access to a value while holding a lock. All accessors are rvalue-ref-qualified, restricting usage to temporaries to prevent accidental lock lifetime extension. A with_ref method is provided for multi-statement critical sections.
This commit is contained in:
parent
696e82db33
commit
c6626f0b70
@ -11,6 +11,7 @@ set(ORC_RT_HEADERS
|
||||
orc-rt/ExecutorAddress.h
|
||||
orc-rt/IntervalMap.h
|
||||
orc-rt/IntervalSet.h
|
||||
orc-rt/LockedAccess.h
|
||||
orc-rt/Math.h
|
||||
orc-rt/MemoryFlags.h
|
||||
orc-rt/QueueingTaskDispatcher.h
|
||||
|
||||
86
orc-rt/include/orc-rt/LockedAccess.h
Normal file
86
orc-rt/include/orc-rt/LockedAccess.h
Normal file
@ -0,0 +1,86 @@
|
||||
//===---------- LockedAccess.h - Locked access wrapper ----------*- C++ -*-===//
|
||||
//
|
||||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
// See https://llvm.org/LICENSE.txt for license information.
|
||||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// Convenience wrapper for simple locked access to a value.
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#ifndef ORC_RT_LOCKEDACCESS_H
|
||||
#define ORC_RT_LOCKEDACCESS_H
|
||||
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
|
||||
namespace orc_rt {
|
||||
|
||||
/// A convenience wrapper for simple locked access to a value.
|
||||
///
|
||||
/// LockedAccess acquires a lock on construction and releases it on
|
||||
/// destruction, providing pointer-like access to the value in between.
|
||||
/// All accessors are rvalue-ref-qualified, so LockedAccess can only be used
|
||||
/// as a temporary — it cannot be stored in a variable or member.
|
||||
///
|
||||
/// This is intended for simple, short critical sections where a class wants
|
||||
/// to return locked access to an internal value. For more complex locking
|
||||
/// patterns (e.g. lock/unlock/relock, condition variables, multiple locks)
|
||||
/// use std::unique_lock or std::scoped_lock directly.
|
||||
template <typename T, typename LockT,
|
||||
typename MutexT = typename LockT::mutex_type>
|
||||
class LockedAccess {
|
||||
public:
|
||||
/// Construct a LockedAccess that references \p R and locks \p M.
|
||||
LockedAccess(T &R, MutexT &M) : Lock(M), R(R) {}
|
||||
|
||||
// LockedAccess is not copyable or movable.
|
||||
LockedAccess(const LockedAccess &) = delete;
|
||||
LockedAccess &operator=(const LockedAccess &) = delete;
|
||||
LockedAccess(LockedAccess &&) = delete;
|
||||
LockedAccess &operator=(LockedAccess &&) = delete;
|
||||
|
||||
/// Returns a reference to the locked value. The returned reference must not
|
||||
/// be used after this LockedAccess temporary is destroyed, as the lock will
|
||||
/// no longer be held.
|
||||
T &operator*() && noexcept { return R; }
|
||||
const T &operator*() const && noexcept { return R; }
|
||||
|
||||
/// Returns a pointer to the locked value for member access. The pointer must
|
||||
/// not be used after this LockedAccess temporary is destroyed, as the lock
|
||||
/// will no longer be held.
|
||||
T *operator->() && noexcept { return &R; }
|
||||
const T *operator->() const && noexcept { return &R; }
|
||||
|
||||
/// Calls \p Op with a mutable reference to the locked value, returning
|
||||
/// whatever \p Op returns. The lock is held for the duration of the call.
|
||||
/// Use this for multi-statement critical sections.
|
||||
template <typename OpT>
|
||||
decltype(auto)
|
||||
with_ref(OpT &&Op) && noexcept(noexcept(std::forward<OpT>(Op)(R))) {
|
||||
return std::forward<OpT>(Op)(R);
|
||||
}
|
||||
|
||||
/// Calls \p Op with a const reference to the locked value, returning
|
||||
/// whatever \p Op returns. The lock is held for the duration of the call.
|
||||
template <typename OpT>
|
||||
decltype(auto) with_ref(OpT &&Op) const && noexcept(
|
||||
noexcept(std::forward<OpT>(Op)(std::as_const(R)))) {
|
||||
return std::forward<OpT>(Op)(std::as_const(R));
|
||||
}
|
||||
|
||||
private:
|
||||
LockT Lock;
|
||||
T &R;
|
||||
};
|
||||
|
||||
/// Deduction guide: defaults LockT to std::scoped_lock<MutexT>.
|
||||
template <typename T, typename MutexT>
|
||||
LockedAccess(T &, MutexT &)
|
||||
-> LockedAccess<T, std::scoped_lock<MutexT>, MutexT>;
|
||||
|
||||
} // namespace orc_rt
|
||||
|
||||
#endif // ORC_RT_LOCKEDACCESS_H
|
||||
@ -22,6 +22,7 @@ add_orc_rt_unittest(CoreTests
|
||||
ExecutorAddressTest.cpp
|
||||
IntervalMapTest.cpp
|
||||
IntervalSetTest.cpp
|
||||
LockedAccessTest.cpp
|
||||
MathTest.cpp
|
||||
MemoryFlagsTest.cpp
|
||||
QueueingTaskDispatcherTest.cpp
|
||||
|
||||
151
orc-rt/unittests/LockedAccessTest.cpp
Normal file
151
orc-rt/unittests/LockedAccessTest.cpp
Normal file
@ -0,0 +1,151 @@
|
||||
//===- LockedAccessTest.cpp -----------------------------------------------===//
|
||||
//
|
||||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
// See https://llvm.org/LICENSE.txt for license information.
|
||||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// Tests for orc-rt's LockedAccess.h APIs.
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#include "orc-rt/LockedAccess.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
|
||||
using namespace orc_rt;
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename T> struct Foo {
|
||||
std::mutex M;
|
||||
T Val{};
|
||||
|
||||
Foo() = default;
|
||||
Foo(T Val) : Val(std::move(Val)) {}
|
||||
|
||||
auto get() { return LockedAccess(Val, M); }
|
||||
|
||||
auto getConst() { return LockedAccess(std::as_const(Val), M); }
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
TEST(LockedAccessTest, ArrowRead) {
|
||||
Foo<std::pair<int, int>> F({42, 7});
|
||||
EXPECT_EQ(F.get()->first, 42);
|
||||
EXPECT_EQ(F.get()->second, 7);
|
||||
}
|
||||
|
||||
TEST(LockedAccessTest, ArrowWrite) {
|
||||
Foo<std::pair<int, int>> F;
|
||||
F.get()->first = 10;
|
||||
F.get()->second = 20;
|
||||
EXPECT_EQ(F.Val.first, 10);
|
||||
EXPECT_EQ(F.Val.second, 20);
|
||||
}
|
||||
|
||||
TEST(LockedAccessTest, ConstArrowReturnsConstPtr) {
|
||||
Foo<int> F(42);
|
||||
static_assert(std::is_const_v<
|
||||
std::remove_pointer_t<decltype(F.getConst().operator->())>>,
|
||||
"const overload should return const pointer");
|
||||
}
|
||||
|
||||
TEST(LockedAccessTest, DerefRead) {
|
||||
Foo<int> F(42);
|
||||
EXPECT_EQ(*F.get(), 42);
|
||||
}
|
||||
|
||||
TEST(LockedAccessTest, DerefWrite) {
|
||||
Foo<int> F;
|
||||
*F.get() = 7;
|
||||
EXPECT_EQ(F.Val, 7);
|
||||
}
|
||||
|
||||
TEST(LockedAccessTest, DerefPassToFunction) {
|
||||
Foo<int> F(42);
|
||||
auto timesTwo = [](int &X) { return X * 2; };
|
||||
EXPECT_EQ(timesTwo(*F.get()), 84);
|
||||
}
|
||||
|
||||
TEST(LockedAccessTest, ConstDerefReturnsConstRef) {
|
||||
Foo<int> F(42);
|
||||
static_assert(
|
||||
std::is_const_v<std::remove_reference_t<decltype(*F.getConst())>>,
|
||||
"const overload should return const reference");
|
||||
}
|
||||
|
||||
TEST(LockedAccessTest, WithRefRead) {
|
||||
Foo<int> F(42);
|
||||
F.get().with_ref([](int &Y) { EXPECT_EQ(Y, 42); });
|
||||
}
|
||||
|
||||
TEST(LockedAccessTest, WithRefWrite) {
|
||||
Foo<int> F;
|
||||
F.get().with_ref([](int &Y) { Y = 7; });
|
||||
EXPECT_EQ(F.Val, 7);
|
||||
}
|
||||
|
||||
TEST(LockedAccessTest, WithRefReturnValue) {
|
||||
Foo<int> F(42);
|
||||
int Result = F.get().with_ref([](int &Y) { return Y + 1; });
|
||||
EXPECT_EQ(Result, 43);
|
||||
}
|
||||
|
||||
TEST(LockedAccessTest, WithRefReturnReference) {
|
||||
Foo<std::pair<int, int>> F({1, 2});
|
||||
int &Ref = F.get().with_ref(
|
||||
[](std::pair<int, int> &P) -> int & { return P.second; });
|
||||
EXPECT_EQ(&Ref, &F.Val.second);
|
||||
}
|
||||
|
||||
TEST(LockedAccessTest, ConstWithRefGetsConstReference) {
|
||||
Foo<int> F(42);
|
||||
F.getConst().with_ref([](const int &Y) { EXPECT_EQ(Y, 42); });
|
||||
}
|
||||
|
||||
TEST(LockedAccessTest, WithRefMultiStatement) {
|
||||
Foo<std::pair<int, int>> F;
|
||||
F.get().with_ref([](std::pair<int, int> &P) {
|
||||
P.first = 10;
|
||||
P.second = P.first + 5;
|
||||
});
|
||||
EXPECT_EQ(F.Val.first, 10);
|
||||
EXPECT_EQ(F.Val.second, 15);
|
||||
}
|
||||
|
||||
TEST(LockedAccessTest, HoldsLockDuringArrow) {
|
||||
struct Checker {
|
||||
Foo<Checker> *F;
|
||||
bool isLocked() { return !F->M.try_lock(); }
|
||||
};
|
||||
Foo<Checker> F({&F});
|
||||
EXPECT_TRUE(F.get()->isLocked());
|
||||
}
|
||||
|
||||
TEST(LockedAccessTest, HoldsLockDuringWithRef) {
|
||||
Foo<int> F;
|
||||
EXPECT_TRUE(F.get().with_ref([&](int &) { return !F.M.try_lock(); }));
|
||||
}
|
||||
|
||||
TEST(LockedAccessTest, ProtectsAcrossThreads) {
|
||||
Foo<int> F;
|
||||
const int Iters = 10000;
|
||||
|
||||
auto Increment = [&]() {
|
||||
for (int I = 0; I < Iters; ++I)
|
||||
F.get().with_ref([](int &C) { ++C; });
|
||||
};
|
||||
|
||||
std::thread T1(Increment);
|
||||
std::thread T2(Increment);
|
||||
T1.join();
|
||||
T2.join();
|
||||
|
||||
EXPECT_EQ(F.Val, 2 * Iters);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user