[K/N][Runtime] Support thread state switching in spin locks

Issue #KT-49013 Fixed
This commit is contained in:
Ilya Matveev
2021-09-28 17:29:42 +07:00
committed by Space
parent bee44c6e0f
commit 629ea1036e
15 changed files with 223 additions and 50 deletions
@@ -227,14 +227,14 @@ class CycleDetector : private kotlin::Pinned, public KonanAllocatorAware {
} }
void insertCandidate(KRef candidate) { void insertCandidate(KRef candidate) {
std::lock_guard<kotlin::SpinLock> guard(lock_); std::lock_guard guard(lock_);
auto it = candidateList_.insert(candidateList_.begin(), candidate); auto it = candidateList_.insert(candidateList_.begin(), candidate);
candidateInList_.emplace(candidate, it); candidateInList_.emplace(candidate, it);
} }
void removeCandidate(KRef candidate) { void removeCandidate(KRef candidate) {
std::lock_guard<kotlin::SpinLock> guard(lock_); std::lock_guard guard(lock_);
auto it = candidateInList_.find(candidate); auto it = candidateInList_.find(candidate);
if (it == candidateInList_.end()) if (it == candidateInList_.end())
@@ -243,7 +243,7 @@ class CycleDetector : private kotlin::Pinned, public KonanAllocatorAware {
candidateInList_.erase(it); candidateInList_.erase(it);
} }
kotlin::SpinLock lock_; kotlin::SpinLock<kotlin::MutexThreadStateHandling::kIgnore> lock_;
using CandidateList = KStdList<KRef>; using CandidateList = KStdList<KRef>;
CandidateList candidateList_; CandidateList candidateList_;
KStdUnorderedMap<KRef, CandidateList::iterator> candidateInList_; KStdUnorderedMap<KRef, CandidateList::iterator> candidateInList_;
@@ -2994,7 +2994,7 @@ ScopedRefHolder::~ScopedRefHolder() {
CycleDetectorRootset CycleDetector::collectRootset() { CycleDetectorRootset CycleDetector::collectRootset() {
auto& detector = instance(); auto& detector = instance();
CycleDetectorRootset rootset; CycleDetectorRootset rootset;
std::lock_guard<kotlin::SpinLock> guard(detector.lock_); std::lock_guard guard(detector.lock_);
for (auto* candidate: detector.candidateList_) { for (auto* candidate: detector.candidateList_) {
// Only frozen candidates are to be analyzed. // Only frozen candidates are to be analyzed.
if (!isPermanentOrFrozen(candidate)) if (!isPermanentOrFrozen(candidate))
@@ -47,18 +47,6 @@ void ThrowException(KRef exception) {
namespace { namespace {
// This function accesses a TLS variable under the hood, so it must not be called from a thread destructor
// (see kotlin::mm::IsCurrentThreadRegistered, kotlin::mm::GetMemoryState()).
[[nodiscard]] kotlin::ThreadStateGuard setNativeStateForRegisteredThread(bool reentrant = true) {
if (kotlin::mm::IsCurrentThreadRegistered()) {
return kotlin::ThreadStateGuard(kotlin::ThreadState::kNative, reentrant);
} else {
// The current thread is not registered in the Kotlin runtime,
// just return an empty guard which doesn't actually switch the state.
return kotlin::ThreadStateGuard();
}
}
class { class {
/** /**
* Timeout 5 sec for concurrent (second) terminate attempt to give a chance the first one to finish. * Timeout 5 sec for concurrent (second) terminate attempt to give a chance the first one to finish.
@@ -73,7 +61,7 @@ class {
// block() is supposed to be NORETURN, otherwise go to normal abort() // block() is supposed to be NORETURN, otherwise go to normal abort()
konan::abort(); konan::abort();
} else { } else {
auto guard = setNativeStateForRegisteredThread(); kotlin::NativeOrUnregisteredThreadGuard guard(/* reentrant = */ true);
sleep(timeoutSec); sleep(timeoutSec);
// We come here when another terminate handler hangs for 5 sec, that looks fatally broken. Go to forced exit now. // We come here when another terminate handler hangs for 5 sec, that looks fatally broken. Go to forced exit now.
} }
@@ -147,12 +135,12 @@ class TerminateHandler : private kotlin::Pinned {
terminateWithUnhandledException(e.GetExceptionObject()); terminateWithUnhandledException(e.GetExceptionObject());
} catch (...) { } catch (...) {
// Not a Kotlin exception - call default handler // Not a Kotlin exception - call default handler
auto guard = setNativeStateForRegisteredThread(); kotlin::NativeOrUnregisteredThreadGuard guard(/* reentrant = */ true);
queuedHandler(); queuedHandler();
} }
} }
// Come here in case of direct terminate() call or unknown exception - go to default terminate handler. // Come here in case of direct terminate() call or unknown exception - go to default terminate handler.
auto guard = setNativeStateForRegisteredThread(); kotlin::NativeOrUnregisteredThreadGuard guard(/* reentrant = */ true);
queuedHandler(); queuedHandler();
} }
@@ -497,6 +497,20 @@ ALWAYS_INLINE inline R CallWithThreadState(R(*function)(Args...), Args... args)
return function(std::forward<Args>(args)...); return function(std::forward<Args>(args)...);
} }
class NativeOrUnregisteredThreadGuard final : private MoveOnly {
public:
explicit NativeOrUnregisteredThreadGuard(bool reentrant = false) noexcept {
// The default ctor of ThreadStateGuard doesn't set the state.
// So the actual state switching is performed only if the thread is registered.
if (kotlin::mm::IsCurrentThreadRegistered()) {
backingGuard_ = kotlin::ThreadStateGuard(kotlin::ThreadState::kNative, reentrant);
}
}
private:
ThreadStateGuard backingGuard_;
};
extern const bool kSupportsMultipleMutators; extern const bool kSupportsMultipleMutators;
} // namespace kotlin } // namespace kotlin
@@ -16,7 +16,7 @@
namespace kotlin { namespace kotlin {
// A queue that is constructed by collecting subqueues from several `Producer`s. // A queue that is constructed by collecting subqueues from several `Producer`s.
template <typename T> template <typename T, typename Mutex>
class MultiSourceQueue { class MultiSourceQueue {
public: public:
class Producer; class Producer;
@@ -66,7 +66,7 @@ public:
for (auto& node : queue_) { for (auto& node : queue_) {
node.owner_ = nullptr; node.owner_ = nullptr;
} }
std::lock_guard<SpinLock> guard(owner_.mutex_); std::lock_guard<Mutex> guard(owner_.mutex_);
owner_.queue_.splice(owner_.queue_.end(), queue_); owner_.queue_.splice(owner_.queue_.end(), queue_);
owner_.deletionQueue_.splice(owner_.deletionQueue_.end(), deletionQueue_); owner_.deletionQueue_.splice(owner_.deletionQueue_.end(), deletionQueue_);
} }
@@ -114,7 +114,7 @@ public:
explicit Iterable(MultiSourceQueue& owner) noexcept : owner_(owner), guard_(owner_.mutex_) {} explicit Iterable(MultiSourceQueue& owner) noexcept : owner_(owner), guard_(owner_.mutex_) {}
MultiSourceQueue& owner_; // weak MultiSourceQueue& owner_; // weak
std::unique_lock<SpinLock> guard_; std::unique_lock<Mutex> guard_;
}; };
// Lock `MultiSourceQueue` for safe iteration. If element was scheduled for deletion, // Lock `MultiSourceQueue` for safe iteration. If element was scheduled for deletion,
@@ -123,7 +123,7 @@ public:
// Lock `MultiSourceQueue` and apply deletions. Only deletes elements that were published. // Lock `MultiSourceQueue` and apply deletions. Only deletes elements that were published.
void ApplyDeletions() noexcept { void ApplyDeletions() noexcept {
std::lock_guard<SpinLock> guard(mutex_); std::lock_guard<Mutex> guard(mutex_);
KStdList<Node*> remainingDeletions; KStdList<Node*> remainingDeletions;
auto it = deletionQueue_.begin(); auto it = deletionQueue_.begin();
@@ -154,7 +154,7 @@ private:
// which is important for GC mark phase. // which is important for GC mark phase.
KStdList<Node> queue_; KStdList<Node> queue_;
KStdList<Node*> deletionQueue_; KStdList<Node*> deletionQueue_;
SpinLock mutex_; Mutex mutex_;
}; };
} // namespace kotlin } // namespace kotlin
@@ -18,8 +18,8 @@ using namespace kotlin;
namespace { namespace {
template <typename T> template <typename T, typename Mutex>
KStdVector<T> Collect(MultiSourceQueue<T>& queue) { KStdVector<T> Collect(MultiSourceQueue<T, Mutex>& queue) {
KStdVector<T> result; KStdVector<T> result;
for (const auto& element : queue.LockForIter()) { for (const auto& element : queue.LockForIter()) {
result.push_back(element); result.push_back(element);
@@ -29,7 +29,7 @@ KStdVector<T> Collect(MultiSourceQueue<T>& queue) {
} // namespace } // namespace
using IntQueue = MultiSourceQueue<int>; using IntQueue = MultiSourceQueue<int, SpinLock<MutexThreadStateHandling::kIgnore>>;
TEST(MultiSourceQueueTest, Insert) { TEST(MultiSourceQueueTest, Insert) {
IntQueue queue; IntQueue queue;
+36 -1
View File
@@ -20,11 +20,20 @@
#include <cstdint> #include <cstdint>
#include "KAssert.h" #include "KAssert.h"
#include "Memory.h"
#include "Utils.hpp" #include "Utils.hpp"
namespace kotlin { namespace kotlin {
class SpinLock : private Pinned { enum class MutexThreadStateHandling {
kIgnore, kSwitchIfRegistered
};
template <MutexThreadStateHandling threadStateHandling>
class SpinLock;
template <>
class SpinLock<MutexThreadStateHandling::kIgnore> : private Pinned {
public: public:
void lock() noexcept { void lock() noexcept {
while (!__sync_bool_compare_and_swap(&atomicInt, 0, 1)) { while (!__sync_bool_compare_and_swap(&atomicInt, 0, 1)) {
@@ -43,6 +52,32 @@ private:
int32_t atomicInt = 0; int32_t atomicInt = 0;
}; };
template <>
class SpinLock<MutexThreadStateHandling::kSwitchIfRegistered> : private Pinned {
public:
void lock() noexcept {
// Fast path without thread state switching.
if (__sync_bool_compare_and_swap(&atomicInt, 0, 1)) {
return;
}
kotlin::NativeOrUnregisteredThreadGuard guard(/* reentrant = */ true);
while (!__sync_bool_compare_and_swap(&atomicInt, 0, 1)) {
// TODO: yield.
}
}
void unlock() noexcept {
if (!__sync_bool_compare_and_swap(&atomicInt, 1, 0)) {
RuntimeAssert(false, "Unable to unlock");
}
}
private:
// TODO: Consider using `std::atomic_flag`.
int32_t atomicInt = 0;
};
} // namespace kotlin } // namespace kotlin
#endif // RUNTIME_MUTEX_H #endif // RUNTIME_MUTEX_H
@@ -0,0 +1,49 @@
/*
* Copyright 2010-2021 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 "Mutex.hpp"
#include <mutex>
#include <thread>
#include "gtest/gtest.h"
#include "TestSupport.hpp"
using namespace kotlin;
template <typename T>
class MutexTest : public testing::Test {};
using LockTypes = testing::Types<SpinLock<MutexThreadStateHandling::kIgnore>, SpinLock<MutexThreadStateHandling::kSwitchIfRegistered>>;
TYPED_TEST_SUITE(MutexTest, LockTypes);
TYPED_TEST(MutexTest, SmokeDetachedThread) {
using LockUnderTest = TypeParam;
LockUnderTest lock;
std::thread secondThread;
std::atomic<bool> started = false;
std::atomic<int32_t> protectedCounter = 0;
{
std::unique_lock guard1(lock);
secondThread = std::thread([&lock, &started, &protectedCounter]() {
started = true;
std::unique_lock guard2(lock);
protectedCounter++;
});
protectedCounter++;
while (!started) { std::this_thread::yield(); }
// Wait to give the second thread a chance to try to acquire the lock.
std::this_thread::sleep_for(std::chrono::milliseconds(100));
EXPECT_EQ(protectedCounter, 1);
}
secondThread.join();
EXPECT_EQ(protectedCounter, 2);
}
@@ -982,7 +982,7 @@ static const TypeInfo* createTypeInfo(Class clazz, const TypeInfo* superType, co
return result; return result;
} }
static kotlin::SpinLock typeInfoCreationMutex; static kotlin::SpinLock<kotlin::MutexThreadStateHandling::kSwitchIfRegistered> typeInfoCreationMutex;
static const TypeInfo* getOrCreateTypeInfo(Class clazz) { static const TypeInfo* getOrCreateTypeInfo(Class clazz) {
const TypeInfo* result = Kotlin_ObjCExport_getAssociatedTypeInfo(clazz); const TypeInfo* result = Kotlin_ObjCExport_getAssociatedTypeInfo(clazz);
@@ -996,7 +996,7 @@ static const TypeInfo* getOrCreateTypeInfo(Class clazz) {
theForeignObjCObjectTypeInfo : theForeignObjCObjectTypeInfo :
getOrCreateTypeInfo(superClass); getOrCreateTypeInfo(superClass);
std::lock_guard<kotlin::SpinLock> lockGuard(typeInfoCreationMutex); std::lock_guard lockGuard(typeInfoCreationMutex);
result = Kotlin_ObjCExport_getAssociatedTypeInfo(clazz); // double-checking. result = Kotlin_ObjCExport_getAssociatedTypeInfo(clazz); // double-checking.
if (result == nullptr) { if (result == nullptr) {
@@ -1016,7 +1016,7 @@ const TypeInfo* Kotlin_ObjCExport_createTypeInfoWithKotlinFieldsFrom(Class clazz
return createTypeInfo(clazz, superType, fieldsInfo); return createTypeInfo(clazz, superType, fieldsInfo);
} }
static kotlin::SpinLock classCreationMutex; static kotlin::SpinLock<kotlin::MutexThreadStateHandling::kSwitchIfRegistered> classCreationMutex;
static int anonymousClassNextId = 0; static int anonymousClassNextId = 0;
static void addVirtualAdapters(Class clazz, const ObjCTypeAdapter* typeAdapter) { static void addVirtualAdapters(Class clazz, const ObjCTypeAdapter* typeAdapter) {
@@ -1092,7 +1092,7 @@ static Class getOrCreateClass(const TypeInfo* typeInfo) {
} else { } else {
Class superClass = getOrCreateClass(typeInfo->superType_); Class superClass = getOrCreateClass(typeInfo->superType_);
std::lock_guard<kotlin::SpinLock> lockGuard(classCreationMutex); // Note: non-recursive std::lock_guard lockGuard(classCreationMutex); // Note: non-recursive
result = typeInfo->writableInfo_->objCExport.objCClass; // double-checking. result = typeInfo->writableInfo_->objCExport.objCClass; // double-checking.
if (result == nullptr) { if (result == nullptr) {
@@ -253,7 +253,7 @@ static void AddMethods(Class clazz, const struct ObjCMethodDescription* methods,
} }
} }
static kotlin::SpinLock classCreationMutex; static kotlin::SpinLock<kotlin::MutexThreadStateHandling::kSwitchIfRegistered> classCreationMutex;
static int anonymousClassNextId = 0; static int anonymousClassNextId = 0;
NO_EXTERNAL_CALLS_CHECK static Class allocateClass(const KotlinObjCClassInfo* info) { NO_EXTERNAL_CALLS_CHECK static Class allocateClass(const KotlinObjCClassInfo* info) {
@@ -284,7 +284,7 @@ NO_EXTERNAL_CALLS_CHECK static Class allocateClass(const KotlinObjCClassInfo* in
} }
void* CreateKotlinObjCClass(const KotlinObjCClassInfo* info) { void* CreateKotlinObjCClass(const KotlinObjCClassInfo* info) {
std::lock_guard<kotlin::SpinLock> lockGuard(classCreationMutex); std::lock_guard lockGuard(classCreationMutex);
void* createdClass = *info->createdClass; void* createdClass = *info->createdClass;
if (createdClass != nullptr) { if (createdClass != nullptr) {
@@ -20,7 +20,7 @@
namespace kotlin { namespace kotlin {
// TODO: Consider different locking mechanisms. // TODO: Consider different locking mechanisms.
template <typename Value, typename Mutex = SpinLock> template <typename Value, typename Mutex>
class SingleLockList : private Pinned { class SingleLockList : private Pinned {
public: public:
class Node; class Node;
@@ -19,7 +19,7 @@ using namespace kotlin;
namespace { namespace {
using IntList = SingleLockList<int>; using IntList = SingleLockList<int, SpinLock<MutexThreadStateHandling::kIgnore>>;
} // namespace } // namespace
@@ -376,7 +376,7 @@ private:
} // namespace } // namespace
TEST(SingleLockListTest, PinnedType) { TEST(SingleLockListTest, PinnedType) {
SingleLockList<PinnedType> list; SingleLockList<PinnedType, SpinLock<MutexThreadStateHandling::kIgnore>> list;
constexpr int kFirst = 1; constexpr int kFirst = 1;
auto* itemNode = list.Emplace(kFirst); auto* itemNode = list.Emplace(kFirst);
@@ -414,7 +414,7 @@ private:
TEST(SingleLockListTest, Destructor) { TEST(SingleLockListTest, Destructor) {
testing::StrictMock<testing::MockFunction<DestructorHook>> hook; testing::StrictMock<testing::MockFunction<DestructorHook>> hook;
{ {
SingleLockList<WithDestructorHook> list; SingleLockList<WithDestructorHook, SpinLock<MutexThreadStateHandling::kIgnore>> list;
auto* first = list.Emplace(hook.AsStdFunction())->Get(); auto* first = list.Emplace(hook.AsStdFunction())->Get();
auto* second = list.Emplace(hook.AsStdFunction())->Get(); auto* second = list.Emplace(hook.AsStdFunction())->Get();
auto* third = list.Emplace(hook.AsStdFunction())->Get(); auto* third = list.Emplace(hook.AsStdFunction())->Get();
@@ -15,16 +15,18 @@ namespace kotlin {
namespace mm { namespace mm {
class GlobalsRegistry : Pinned { class GlobalsRegistry : Pinned {
using Mutex = SpinLock<MutexThreadStateHandling::kIgnore>;
public: public:
class ThreadQueue : public MultiSourceQueue<ObjHeader**>::Producer { class ThreadQueue : public MultiSourceQueue<ObjHeader**, Mutex>::Producer {
public: public:
explicit ThreadQueue(GlobalsRegistry& registry) : Producer(registry.globals_) {} explicit ThreadQueue(GlobalsRegistry& registry) : Producer(registry.globals_) {}
// Do not add fields as this is just a wrapper and Producer does not have virtual destructor. // Do not add fields as this is just a wrapper and Producer does not have virtual destructor.
}; };
using Iterable = MultiSourceQueue<ObjHeader**>::Iterable; using Iterable = MultiSourceQueue<ObjHeader**, Mutex>::Iterable;
using Iterator = MultiSourceQueue<ObjHeader**>::Iterator; using Iterator = MultiSourceQueue<ObjHeader**, Mutex>::Iterator;
GlobalsRegistry(); GlobalsRegistry();
~GlobalsRegistry(); ~GlobalsRegistry();
@@ -47,7 +49,7 @@ public:
private: private:
// TODO: Add-only MultiSourceQueue can be made more efficient. Measure, if it's a problem. // TODO: Add-only MultiSourceQueue can be made more efficient. Measure, if it's a problem.
MultiSourceQueue<ObjHeader**> globals_; MultiSourceQueue<ObjHeader**, Mutex> globals_;
}; };
} // namespace mm } // namespace mm
@@ -0,0 +1,83 @@
/*
* Copyright 2010-2021 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 "Mutex.hpp"
#include <thread>
#include "gtest/gtest.h"
#include "TestSupport.hpp"
#include "ThreadState.hpp"
using namespace kotlin;
namespace {
template <MutexThreadStateHandling Handling, ThreadState State>
class Mode {
public:
using Lock = SpinLock<Handling>;
static constexpr MutexThreadStateHandling threadStateHandling = Handling;
static constexpr ThreadState initialState = State;
};
} // namespace
template <typename T>
class MutexTestNewMM : public testing::Test {};
using TestModes = testing::Types<
Mode<MutexThreadStateHandling::kIgnore, ThreadState::kRunnable>,
Mode<MutexThreadStateHandling::kIgnore, ThreadState::kNative>,
Mode<MutexThreadStateHandling::kSwitchIfRegistered, ThreadState::kRunnable>,
Mode<MutexThreadStateHandling::kSwitchIfRegistered, ThreadState::kNative>>;
TYPED_TEST_SUITE(MutexTestNewMM, TestModes);
TYPED_TEST(MutexTestNewMM, SmokeAttachedThread) {
RunInNewThread([](){
using LockUnderTest = typename TypeParam::Lock;
ThreadState initialThreadState = TypeParam::initialState;
LockUnderTest lock;
std::thread secondThread;
std::atomic<bool> started = false;
std::atomic<int32_t> protectedCounter = 0;
mm::ThreadData* secondThreadData = nullptr;
{
std::unique_lock guard1(lock);
secondThread = std::thread([&lock, &started, &protectedCounter, &secondThreadData, &initialThreadState]() {
ScopedMemoryInit init;
SwitchThreadState(init.memoryState(), initialThreadState, /* reentrant = */ true);
secondThreadData = init.memoryState()->GetThreadData();
ASSERT_EQ(secondThreadData->state(), initialThreadState);
started = true;
std::unique_lock guard2(lock);
EXPECT_EQ(secondThreadData->state(), initialThreadState);
protectedCounter++;
});
protectedCounter++;
while (!started) {
std::this_thread::yield();
}
// Wait to give the second thread a chance to try to acquire the lock.
std::this_thread::sleep_for(std::chrono::milliseconds(100));
EXPECT_EQ(protectedCounter, 1);
auto expectedSecondThreadState =
(TypeParam::threadStateHandling == MutexThreadStateHandling::kSwitchIfRegistered)
? ThreadState::kNative
: initialThreadState;
EXPECT_EQ(secondThreadData->state(), expectedSecondThreadState);
}
secondThread.join();
EXPECT_EQ(protectedCounter, 2);
});
}
@@ -163,7 +163,7 @@ public:
return; return;
} }
std::lock_guard<SpinLock> guard(owner_.mutex_); std::lock_guard guard(owner_.mutex_);
owner_.AssertCorrectUnsafe(); owner_.AssertCorrectUnsafe();
@@ -328,7 +328,7 @@ public:
private: private:
ObjectFactoryStorage& owner_; // weak ObjectFactoryStorage& owner_; // weak
std::unique_lock<SpinLock> guard_; std::unique_lock<SpinLock<MutexThreadStateHandling::kIgnore>> guard_;
}; };
~ObjectFactoryStorage() { ~ObjectFactoryStorage() {
@@ -389,7 +389,7 @@ private:
unique_ptr<Node> root_; unique_ptr<Node> root_;
Node* last_ = nullptr; Node* last_ = nullptr;
size_t size_ = 0; size_t size_ = 0;
SpinLock mutex_; SpinLock<MutexThreadStateHandling::kIgnore> mutex_;
}; };
class SimpleAllocator { class SimpleAllocator {
@@ -15,16 +15,18 @@ namespace mm {
// Registry for all objects that have references outside of Kotlin. // Registry for all objects that have references outside of Kotlin.
class StableRefRegistry : Pinned { class StableRefRegistry : Pinned {
using Mutex = SpinLock<MutexThreadStateHandling::kIgnore>;
public: public:
class ThreadQueue : public MultiSourceQueue<ObjHeader*>::Producer { class ThreadQueue : public MultiSourceQueue<ObjHeader*, Mutex>::Producer {
public: public:
explicit ThreadQueue(StableRefRegistry& registry) : Producer(registry.stableRefs_) {} explicit ThreadQueue(StableRefRegistry& registry) : Producer(registry.stableRefs_) {}
// Do not add fields as this is just a wrapper and Producer does not have virtual destructor. // Do not add fields as this is just a wrapper and Producer does not have virtual destructor.
}; };
using Iterable = MultiSourceQueue<ObjHeader*>::Iterable; using Iterable = MultiSourceQueue<ObjHeader*, Mutex>::Iterable;
using Iterator = MultiSourceQueue<ObjHeader*>::Iterator; using Iterator = MultiSourceQueue<ObjHeader*, Mutex>::Iterator;
using Node = MultiSourceQueue<ObjHeader*>::Node; using Node = MultiSourceQueue<ObjHeader*, Mutex>::Node;
StableRefRegistry(); StableRefRegistry();
~StableRefRegistry(); ~StableRefRegistry();
@@ -64,7 +66,7 @@ private:
// before posting queue to the global registry) // before posting queue to the global registry)
// //
// TODO: Measure to understand, if this approach is problematic. // TODO: Measure to understand, if this approach is problematic.
MultiSourceQueue<ObjHeader*> stableRefs_; MultiSourceQueue<ObjHeader*, Mutex> stableRefs_;
}; };
} // namespace mm } // namespace mm