[K/N][Runtime] Support suspending mutators in the new MM

This commit is contained in:
Ilya Matveev
2021-05-31 09:48:46 +00:00
committed by Space
parent 94a94a557d
commit de5dcfcbd8
11 changed files with 466 additions and 12 deletions
+6
View File
@@ -272,6 +272,12 @@ val assemble by tasks.registering {
})
}
val hostAssemble by tasks.registering {
dependsOn(tasks.withType(CompileToBitcode::class).matching {
it.outputGroup == "main" && it.target == hostName
})
}
val clean by tasks.registering {
doFirst {
delete(buildDir)
@@ -3733,6 +3733,10 @@ ALWAYS_INLINE void kotlin::AssertThreadState(MemoryState* thread, ThreadState ex
// no-op, used by the new MM only.
}
ALWAYS_INLINE void kotlin::AssertThreadState(MemoryState* thread, std::initializer_list<ThreadState> expected) noexcept {
// no-op, used by the new MM only.
}
MemoryState* kotlin::mm::GetMemoryState() {
return ::memoryState;
}
@@ -17,6 +17,7 @@
#ifndef RUNTIME_MEMORY_H
#define RUNTIME_MEMORY_H
#include <ostream>
#include <utility>
#include "KAssert.h"
@@ -410,12 +411,17 @@ ALWAYS_INLINE ThreadState SwitchThreadState(MemoryState* thread, ThreadState new
// Asserts that the given thread is in the given state.
ALWAYS_INLINE void AssertThreadState(MemoryState* thread, ThreadState expected) noexcept;
ALWAYS_INLINE void AssertThreadState(MemoryState* thread, std::initializer_list<ThreadState> expected) noexcept;
// Asserts that the current thread is in the the given state.
ALWAYS_INLINE inline void AssertThreadState(ThreadState expected) noexcept {
AssertThreadState(mm::GetMemoryState(), expected);
}
ALWAYS_INLINE inline void AssertThreadState(std::initializer_list<ThreadState> expected) noexcept {
AssertThreadState(mm::GetMemoryState(), expected);
}
// Scopely sets the given thread state for the given thread.
class ThreadStateGuard final : private Pinned {
public:
@@ -6,6 +6,8 @@
#ifndef RUNTIME_UTILS_H
#define RUNTIME_UTILS_H
#include <type_traits>
namespace kotlin {
// A helper for implementing classes with disabled copy constructor and copy assignment.
@@ -52,6 +54,28 @@ protected:
~Pinned() = default;
};
// A helper that scopley assings a value to a variable. The variable will
// be set to its original value upon destruction of the AutoReset instance.
// Note that an AutoReset instance must have a shorter lifetime than
// the variable it works with to avoid invalid memory access.
template<typename T1, typename T2>
class AutoReset final : private Pinned {
static_assert(std::is_assignable<T1, T2>::value);
public:
AutoReset(T1* variable, T2 value) : variable_(variable), oldValue_(*variable) {
*variable_ = value;
}
~AutoReset() {
*variable_ = oldValue_;
}
private:
T1* variable_;
T2 oldValue_;
};
} // namespace kotlin
#endif // RUNTIME_UTILS_H
@@ -18,6 +18,7 @@
#include "ThreadLocalStorage.hpp"
#include "Types.h"
#include "Utils.hpp"
#include "ThreadSuspension.hpp"
struct ObjHeader;
@@ -32,9 +33,9 @@ public:
threadId_(threadId),
globalsThreadQueue_(GlobalsRegistry::Instance()),
stableRefThreadQueue_(StableRefRegistry::Instance()),
state_(ThreadState::kRunnable),
gc_(GlobalData::Instance().gc()),
objectFactoryThreadQueue_(GlobalData::Instance().objectFactory(), gc_) {}
objectFactoryThreadQueue_(GlobalData::Instance().objectFactory(), gc_),
suspensionData_(ThreadState::kRunnable) {}
~ThreadData() = default;
@@ -46,9 +47,9 @@ public:
StableRefRegistry::ThreadQueue& stableRefThreadQueue() noexcept { return stableRefThreadQueue_; }
ThreadState state() noexcept { return state_; }
ThreadState state() noexcept { return suspensionData_.state(); }
ThreadState setState(ThreadState state) noexcept { return state_.exchange(state); }
ThreadState setState(ThreadState state) noexcept { return suspensionData_.setState(state); }
ObjectFactory<gc::GC>::ThreadQueue& objectFactoryThreadQueue() noexcept { return objectFactoryThreadQueue_; }
@@ -58,6 +59,8 @@ public:
gc::GC::ThreadData& gc() noexcept { return gc_; }
ThreadSuspensionData& suspensionData() { return suspensionData_; }
void Publish() noexcept {
// TODO: These use separate locks, which is inefficient.
globalsThreadQueue_.Publish();
@@ -76,11 +79,11 @@ private:
GlobalsRegistry::ThreadQueue globalsThreadQueue_;
ThreadLocalStorage tls_;
StableRefRegistry::ThreadQueue stableRefThreadQueue_;
std::atomic<ThreadState> state_;
ShadowStack shadowStack_;
gc::GC::ThreadData gc_;
ObjectFactory<gc::GC>::ThreadQueue objectFactoryThreadQueue_;
KStdVector<std::pair<ObjHeader**, ObjHeader*>> initializingSingletons_;
ThreadSuspensionData suspensionData_;
};
} // namespace mm
@@ -7,7 +7,7 @@
#include "ThreadData.hpp"
#include "ThreadState.hpp"
const char* kotlin::internal::stateToString(ThreadState state) noexcept {
const char* kotlin::ThreadStateName(ThreadState state) noexcept {
switch (state) {
case ThreadState::kRunnable:
return "RUNNABLE";
@@ -16,6 +16,18 @@ const char* kotlin::internal::stateToString(ThreadState state) noexcept {
}
}
std::string kotlin::internal::statesToString(std::initializer_list<ThreadState> states) noexcept {
std::string result = "{ ";
for (size_t i = 0; i < states.size(); i++) {
if (i != 0) {
result += ", ";
}
result += ThreadStateName(data(states)[i]);
}
result += " }";
return result;
}
ALWAYS_INLINE ThreadState kotlin::SwitchThreadState(MemoryState* thread, ThreadState newState, bool reentrant) noexcept {
return SwitchThreadState(thread->GetThreadData(), newState, reentrant);
}
@@ -24,6 +36,10 @@ ALWAYS_INLINE void kotlin::AssertThreadState(MemoryState* thread, ThreadState ex
AssertThreadState(thread->GetThreadData(), expected);
}
ALWAYS_INLINE void kotlin::AssertThreadState(MemoryState* thread, std::initializer_list<ThreadState> expected) noexcept {
AssertThreadState(thread->GetThreadData(), expected);
}
ThreadState kotlin::GetThreadState(MemoryState* thread) noexcept {
return thread->GetThreadData()->state();
}
@@ -6,10 +6,13 @@
#ifndef RUNTIME_MM_THREAD_STATE_H
#define RUNTIME_MM_THREAD_STATE_H
#include <ostream>
#include <Common.h>
#include <Utils.hpp>
#include "ThreadData.hpp"
#include "ThreadSuspension.hpp"
namespace kotlin {
@@ -19,17 +22,23 @@ ALWAYS_INLINE inline bool isStateSwitchAllowed(ThreadState oldState, ThreadState
return oldState != newState || reentrant;
}
const char* stateToString(ThreadState state) noexcept;
std::string statesToString(std::initializer_list<ThreadState> states) noexcept;
} // namespace internal
const char* ThreadStateName(ThreadState state) noexcept;
inline std::ostream& operator<<(std::ostream& stream, ThreadState state) {
return stream << ThreadStateName(state);
}
// Switches the state of the given thread to `newState` and returns the previous thread state.
ALWAYS_INLINE inline ThreadState SwitchThreadState(mm::ThreadData* threadData, ThreadState newState, bool reentrant = false) noexcept {
auto oldState = threadData->setState(newState);
// TODO(perf): Mesaure the impact of this assert in debug and opt modes.
RuntimeAssert(internal::isStateSwitchAllowed(oldState, newState, reentrant),
"Illegal thread state switch. Old state: %s. New state: %s.",
internal::stateToString(oldState), internal::stateToString(newState));
ThreadStateName(oldState), ThreadStateName(newState));
return oldState;
}
@@ -38,7 +47,14 @@ ALWAYS_INLINE inline void AssertThreadState(mm::ThreadData* threadData, ThreadSt
auto actual = threadData->state();
RuntimeAssert(actual == expected,
"Unexpected thread state. Expected: %s. Actual: %s.",
internal::stateToString(expected), internal::stateToString(actual));
ThreadStateName(expected), ThreadStateName(actual));
}
ALWAYS_INLINE inline void AssertThreadState(mm::ThreadData* threadData, std::initializer_list<ThreadState> expected) noexcept {
auto actual = threadData->state();
RuntimeAssert(std::any_of(expected.begin(), expected.end(), [actual](ThreadState expected) { return expected == actual; }),
"Unexpected thread state. Expected one of: %s. Actual: %s",
internal::statesToString(expected).c_str(), ThreadStateName(actual));
}
} // namespace kotlin
@@ -41,6 +41,9 @@ private:
//static
testing::MockFunction<int32_t(int32_t)>* ThreadStateTest::globalSomeFunctionMock = nullptr;
#define EXPECT_NO_DEATH(statement) \
do { EXPECT_EXIT({statement; exit(0);}, testing::ExitedWithCode(0), testing::_); } while(false)
} // namespace
TEST_F(ThreadStateTest, StateSwitchWithThreadData) {
@@ -148,13 +151,11 @@ TEST(ThreadStateDeathTest, StateAsserts) {
});
}
TEST(ThreadStateDeathTest, IncorrectStateSwitch) {
TEST(ThreadStateDeathTest, IncorrectStateSwitchWithDifferentFunctions) {
RunInNewThread([](MemoryState* memoryState) {
auto* threadData = memoryState->GetThreadData();
EXPECT_DEATH(SwitchThreadState(memoryState, ThreadState::kRunnable),
"runtime assert: Illegal thread state switch. Old state: RUNNABLE. New state: RUNNABLE");
EXPECT_DEATH(SwitchThreadState(threadData, ThreadState::kRunnable),
"runtime assert: Illegal thread state switch. Old state: RUNNABLE. New state: RUNNABLE");
EXPECT_DEATH(Kotlin_mm_switchThreadStateRunnable(),
"runtime assert: Illegal thread state switch. Old state: RUNNABLE. New state: RUNNABLE");
@@ -165,6 +166,24 @@ TEST(ThreadStateDeathTest, IncorrectStateSwitch) {
});
}
TEST(ThreadStateDeathTest, StateSwitchCorrectness) {
mm::ThreadData threadData(pthread_self());
// Allowed state switches: runnable <-> native
threadData.setState(ThreadState::kRunnable);
ASSERT_EQ(threadData.state(), ThreadState::kRunnable);
EXPECT_DEATH(SwitchThreadState(&threadData, ThreadState::kRunnable),
"runtime assert: Illegal thread state switch. Old state: RUNNABLE. New state: RUNNABLE");
// Each EXPECT_NO_DEATH is executed in a fork process, so the global state of the test is not affected.
EXPECT_NO_DEATH(SwitchThreadState(&threadData, ThreadState::kNative));
threadData.setState(ThreadState::kNative);
ASSERT_EQ(threadData.state(), ThreadState::kNative);
EXPECT_NO_DEATH(SwitchThreadState(&threadData, ThreadState::kRunnable));
EXPECT_DEATH(SwitchThreadState(&threadData, ThreadState::kNative),
"runtime assert: Illegal thread state switch. Old state: NATIVE. New state: NATIVE");
}
TEST(ThreadStateDeathTest, ReentrantStateSwitch) {
RunInNewThread([](MemoryState* memoryState) {
auto* threadData = memoryState->GetThreadData();
@@ -0,0 +1,95 @@
/*
* 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 "ThreadData.hpp"
#include "ThreadSuspension.hpp"
#include <condition_variable>
#include <thread>
#include <mutex>
namespace {
bool isSuspendedOrNative(kotlin::mm::ThreadData& thread) noexcept {
auto& suspensionData = thread.suspensionData();
return suspensionData.suspended() || suspensionData.state() == kotlin::ThreadState::kNative;
}
template<typename F>
bool allThreads(F predicate) noexcept {
auto& threadRegistry = kotlin::mm::ThreadRegistry::Instance();
auto* currentThread = threadRegistry.CurrentThreadData();
kotlin::mm::ThreadRegistry::Iterable threads = kotlin::mm::ThreadRegistry::Instance().Iter();
for (auto& thread : threads) {
// Handle if suspension was initiated by the mutator thread.
if (&thread == currentThread)
continue;
if (!predicate(thread)) {
return false;
}
}
return true;
}
void yield() noexcept {
std::this_thread::yield();
}
std::atomic<bool> gSuspensionRequested = false;
THREAD_LOCAL_VARIABLE bool gSuspensionRequestedByCurrentThread = false;
std::mutex gSuspensionMutex;
std::condition_variable gSuspendsionCondVar;
} // namespace
bool kotlin::mm::ThreadSuspensionData::suspendIfRequested() noexcept {
if (IsThreadSuspensionRequested()) {
std::unique_lock lock(gSuspensionMutex);
if (IsThreadSuspensionRequested()) {
AutoReset scopedAssign(&suspended_, true);
gSuspendsionCondVar.wait(lock, []() { return !IsThreadSuspensionRequested(); });
return true;
}
}
return false;
}
bool kotlin::mm::IsThreadSuspensionRequested() noexcept {
// TODO: Consider using a more relaxed memory order.
return gSuspensionRequested.load();
}
bool kotlin::mm::SuspendThreads() noexcept {
RuntimeAssert(gSuspensionRequestedByCurrentThread == false, "Current thread already suspended threads.");
{
std::unique_lock lock(gSuspensionMutex);
bool actual = false;
gSuspensionRequested.compare_exchange_strong(actual, true);
if (actual) {
return false;
}
}
gSuspensionRequestedByCurrentThread = true;
// Spin wating for threads to suspend. Ignore Native threads.
while(!allThreads(isSuspendedOrNative)) {
yield();
}
return true;
}
void kotlin::mm::ResumeThreads() noexcept {
// From the std::condition_variable docs:
// Even if the shared variable is atomic, it must be modified under
// the mutex in order to correctly publish the modification to the waiting thread.
// https://en.cppreference.com/w/cpp/thread/condition_variable
{
std::unique_lock lock(gSuspensionMutex);
gSuspensionRequested = false;
}
gSuspensionRequestedByCurrentThread = false;
gSuspendsionCondVar.notify_all();
}
@@ -0,0 +1,60 @@
/*
* 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.
*/
#ifndef RUNTIME_MM_THREAD_SUSPENSION_UTILS_H
#define RUNTIME_MM_THREAD_SUSPENSION_UTILS_H
#include <atomic>
#include "Memory.h"
namespace kotlin {
namespace mm {
class ThreadSuspensionData : private Pinned {
public:
explicit ThreadSuspensionData(ThreadState initialState) noexcept : state_(initialState), suspended_(false) {}
~ThreadSuspensionData() = default;
ThreadState state() noexcept { return state_; }
ThreadState setState(ThreadState newState) noexcept {
ThreadState oldState = state_.exchange(newState);
if (oldState == ThreadState::kNative && newState == ThreadState::kRunnable) {
suspendIfRequested();
}
return oldState;
}
bool suspended() noexcept { return suspended_; }
bool suspendIfRequested() noexcept;
private:
std::atomic<ThreadState> state_;
std::atomic<bool> suspended_;
};
bool IsThreadSuspensionRequested() noexcept;
/**
* Suspends all threads registered in ThreadRegistry except threads that are in the Native state.
* Blocks until all such threads are suspended. Threads that are in the Native state on the moment
* of this call will be suspended on exit from the Native state.
* Returns false if some other thread has suspended the threads.
*/
bool SuspendThreads() noexcept;
/**
* Resumes all threads registered in ThreadRegistry that were suspended by the SuspendThreads call.
* Does not wait until all such threads are actually resumed.
*/
void ResumeThreads() noexcept;
} // namespace mm
} // namespace kotlin
#endif // RUNTIME_MM_THREAD_SUSPENSION_UTILS_H
@@ -0,0 +1,205 @@
/*
* 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 "MemoryPrivate.hpp"
#include "ThreadSuspension.hpp"
#include "ThreadState.hpp"
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <thread>
#include <TestSupport.hpp>
#include <iostream>
using namespace kotlin;
namespace {
#ifdef KONAN_WINDOWS
constexpr size_t kDefaultIterations = 1000;
constexpr size_t kDefaultReportingStep = 100;
#else
constexpr size_t kDefaultIterations = 10000;
constexpr size_t kDefaultReportingStep = 1000;
#endif // #ifdef KONAN_WINDOWS
KStdVector<mm::ThreadData*> collectThreadData() {
KStdVector<mm::ThreadData*> result;
auto iter = mm::ThreadRegistry::Instance().Iter();
for (auto& thread : iter) {
result.push_back(&thread);
}
return result;
}
template<typename T, typename F>
KStdVector<T> collectFromThreadData(F extractFunction) {
KStdVector<T> result;
auto threadData = collectThreadData();
std::transform(threadData.begin(), threadData.end(), std::back_inserter(result), extractFunction);
return result;
}
KStdVector<bool> collectSuspended() {
return collectFromThreadData<bool>(
[](mm::ThreadData* threadData) { return threadData->suspensionData().suspended(); });
}
void reportProgress(size_t currentIteration, size_t totalIterations) {
if (currentIteration % kDefaultReportingStep == 0) {
std::cout << "Iteration: " << currentIteration << " of " << totalIterations << std::endl;
}
}
} // namespace
class ThreadSuspensionTest : public ::testing::Test {
public:
~ThreadSuspensionTest() {
canStart = true;
shouldStop = true;
for (auto& thread : threads) {
if (thread.joinable()) thread.join();
}
}
static constexpr size_t kThreadCount = kDefaultThreadCount;
static constexpr size_t kIterations = kDefaultIterations;
KStdVector<std::thread> threads;
std::array<std::atomic<bool>, kThreadCount> ready{false};
std::atomic<bool> canStart{false};
std::atomic<bool> shouldStop{false};
void waitUntilCanStart(size_t threadNumber) {
ready[threadNumber] = true;
while(!canStart) {
std::this_thread::yield();
}
ready[threadNumber] = false;
}
void waitUntilThreadsAreReady() {
canStart = false;
while (!std::all_of(ready.begin(), ready.end(), [](bool it) { return it; })) {
std::this_thread::yield();
}
}
};
TEST_F(ThreadSuspensionTest, SimpleStartStop) {
ASSERT_THAT(collectThreadData(), testing::IsEmpty());
for (size_t i = 0; i < kThreadCount; i++) {
threads.emplace_back([this, i]() {
ScopedMemoryInit init;
auto& suspensionData = init.memoryState()->GetThreadData()->suspensionData();
EXPECT_EQ(mm::IsThreadSuspensionRequested(), false);
while(!shouldStop) {
waitUntilCanStart(i);
EXPECT_FALSE(suspensionData.suspended());
suspensionData.suspendIfRequested();
EXPECT_FALSE(suspensionData.suspended());
}
});
}
waitUntilThreadsAreReady();
for (size_t i = 0; i < kIterations; i++) {
reportProgress(i, kIterations);
canStart = true;
mm::SuspendThreads();
auto suspended = collectSuspended();
EXPECT_THAT(suspended, testing::Each(true));
EXPECT_EQ(mm::IsThreadSuspensionRequested(), true);
mm::ResumeThreads();
// Wait for threads to run and sync for the next iteration
waitUntilThreadsAreReady();
suspended = collectSuspended();
EXPECT_THAT(suspended, testing::Each(false));
EXPECT_EQ(mm::IsThreadSuspensionRequested(), false);
}
}
TEST_F(ThreadSuspensionTest, SwitchStateToNative) {
ASSERT_THAT(collectThreadData(), testing::IsEmpty());
for (size_t i = 0; i < kThreadCount; i++) {
threads.emplace_back([this, i]() {
ScopedMemoryInit init;
auto* threadData = init.memoryState()->GetThreadData();
EXPECT_EQ(mm::IsThreadSuspensionRequested(), false);
while(!shouldStop) {
waitUntilCanStart(i);
EXPECT_EQ(threadData->state(), ThreadState::kRunnable);
SwitchThreadState(threadData, ThreadState::kNative);
EXPECT_EQ(threadData->state(), ThreadState::kNative);
SwitchThreadState(threadData, ThreadState::kRunnable);
EXPECT_EQ(threadData->state(), ThreadState::kRunnable);
}
});
}
waitUntilThreadsAreReady();
for (size_t i = 0; i < kIterations; i++) {
reportProgress(i, kIterations);
canStart = true;
mm::SuspendThreads();
EXPECT_EQ(mm::IsThreadSuspensionRequested(), true);
mm::ResumeThreads();
EXPECT_EQ(mm::IsThreadSuspensionRequested(), false);
// Sync for the next iteration.
waitUntilThreadsAreReady();
}
}
TEST_F(ThreadSuspensionTest, ConcurrentSuspend) {
ASSERT_THAT(collectThreadData(), testing::IsEmpty());
std::atomic<size_t> successCount = 0;
for (size_t i = 0; i < kThreadCount; i++) {
threads.emplace_back([this, i, &successCount]() {
ScopedMemoryInit init;
auto* currentThreadData = init.memoryState()->GetThreadData();
EXPECT_EQ(mm::IsThreadSuspensionRequested(), false);
// Sync with other threads.
ready[i] = true;
waitUntilThreadsAreReady();
bool success = mm::SuspendThreads();
if (success) {
successCount++;
auto allThreadData = collectThreadData();
auto isCurrentOrSuspended = [currentThreadData](mm::ThreadData* data) {
return data == currentThreadData || data->suspensionData().suspended();
};
EXPECT_THAT(allThreadData, testing::Each(testing::Truly(isCurrentOrSuspended)));
EXPECT_FALSE(currentThreadData->suspensionData().suspended());
mm::ResumeThreads();
} else {
EXPECT_TRUE(mm::IsThreadSuspensionRequested());
currentThreadData->suspensionData().suspendIfRequested();
}
});
}
for (auto& thread : threads) {
thread.join();
}
EXPECT_EQ(successCount, 1u);
}