diff --git a/kotlin-native/runtime/src/main/cpp/Runtime.cpp b/kotlin-native/runtime/src/main/cpp/Runtime.cpp index 365fac3d966..06d74079389 100644 --- a/kotlin-native/runtime/src/main/cpp/Runtime.cpp +++ b/kotlin-native/runtime/src/main/cpp/Runtime.cpp @@ -24,8 +24,14 @@ #include "ObjCExportInit.h" #include "Porting.h" #include "Runtime.h" +#include "RuntimePrivate.hpp" #include "Worker.h" +using kotlin::internal::FILE_NOT_INITIALIZED; +using kotlin::internal::FILE_BEING_INITIALIZED; +using kotlin::internal::FILE_INITIALIZED; +using kotlin::internal::FILE_FAILED_TO_INITIALIZE; + typedef void (*Initializer)(int initialize, MemoryState* memory); struct InitNode { Initializer init; @@ -410,11 +416,6 @@ RUNTIME_NOTHROW void Kotlin_initRuntimeIfNeededFromKotlin() { } } -static constexpr int FILE_NOT_INITIALIZED = 0; -static constexpr int FILE_BEING_INITIALIZED = 1; -static constexpr int FILE_INITIALIZED = 2; -static constexpr int FILE_FAILED_TO_INITIALIZE = 3; - void CallInitGlobalPossiblyLock(int volatile* state, void (*init)()) { int localState = *state; if (localState == FILE_INITIALIZED) return; @@ -423,10 +424,13 @@ void CallInitGlobalPossiblyLock(int volatile* state, void (*init)()) { int threadId = konan::currentThreadId(); if ((localState & 3) == FILE_BEING_INITIALIZED) { if ((localState & ~3) != (threadId << 2)) { + // Switch to the native state to avoid dead-locks. + kotlin::ThreadStateGuard guard(kotlin::ThreadState::kNative); do { localState = *state; if (localState == FILE_FAILED_TO_INITIALIZE) - ThrowFileFailedToInitializeException(); + // Call of a Kotlin function. + kotlin::CallWithThreadState(ThrowFileFailedToInitializeException); } while (localState != FILE_INITIALIZED); } return; @@ -445,10 +449,13 @@ void CallInitGlobalPossiblyLock(int volatile* state, void (*init)()) { #endif *state = FILE_INITIALIZED; } else { + // Switch to the native state to avoid dead-locks. + kotlin::ThreadStateGuard guard(kotlin::ThreadState::kNative); do { localState = *state; if (localState == FILE_FAILED_TO_INITIALIZE) - ThrowFileFailedToInitializeException(); + // Call of a Kotlin function. + kotlin::CallWithThreadState(ThrowFileFailedToInitializeException); } while (localState != FILE_INITIALIZED); } } diff --git a/kotlin-native/runtime/src/main/cpp/RuntimePrivate.hpp b/kotlin-native/runtime/src/main/cpp/RuntimePrivate.hpp new file mode 100644 index 00000000000..f86dcdd307e --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/RuntimePrivate.hpp @@ -0,0 +1,17 @@ +/* + * 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. + */ + +#pragma once + +namespace kotlin { +namespace internal { + +inline constexpr int FILE_NOT_INITIALIZED = 0; +inline constexpr int FILE_BEING_INITIALIZED = 1; +inline constexpr int FILE_INITIALIZED = 2; +inline constexpr int FILE_FAILED_TO_INITIALIZE = 3; + +} // namespace internal +} // namespace kotlin diff --git a/kotlin-native/runtime/src/mm/cpp/ThreadSuspensionTest.cpp b/kotlin-native/runtime/src/mm/cpp/ThreadSuspensionTest.cpp index a0ffc5530b2..d9c836d6e21 100644 --- a/kotlin-native/runtime/src/mm/cpp/ThreadSuspensionTest.cpp +++ b/kotlin-native/runtime/src/mm/cpp/ThreadSuspensionTest.cpp @@ -4,14 +4,18 @@ */ #include "MemoryPrivate.hpp" +#include "Runtime.h" +#include "RuntimePrivate.hpp" #include "ThreadSuspension.hpp" #include "ThreadState.hpp" #include #include +#include #include #include +#include #include @@ -55,6 +59,17 @@ void reportProgress(size_t currentIteration, size_t totalIterations) { } } +testing::MockFunction* initializationMock = nullptr; + +void initializationFunction() { + ASSERT_NE(initializationMock, nullptr); + initializationMock->Call(); +} + +test_support::ScopedMockFunction ScopedInitializationMock() { + return test_support::ScopedMockFunction(&initializationMock); +} + } // namespace class ThreadSuspensionTest : public ::testing::Test { @@ -203,3 +218,56 @@ TEST_F(ThreadSuspensionTest, ConcurrentSuspend) { } EXPECT_EQ(successCount, 1u); } + +TEST_F(ThreadSuspensionTest, FileInitializationWithSuspend) { + ASSERT_THAT(collectThreadData(), testing::IsEmpty()); + ASSERT_FALSE(mm::IsThreadSuspensionRequested()); + + volatile int lock = internal::FILE_NOT_INITIALIZED; + + auto scopedInitializationMock = ScopedInitializationMock(); + EXPECT_CALL(*scopedInitializationMock, Call()).WillOnce([] { + EXPECT_EQ(GetThreadState(), ThreadState::kRunnable); + // Give other threads a chance to call CallInitGlobalPossiblyLock. + std::this_thread::yield(); + mm::SuspendIfRequested(); + }); + + for (size_t i = 0; i < kThreadCount; i++) { + threads.emplace_back([this, i, &lock] { + ScopedMemoryInit init; + auto* threadData = init.memoryState()->GetThreadData(); + ASSERT_EQ(threadData->state(), ThreadState::kRunnable); + + waitUntilCanStart(i); + + CallInitGlobalPossiblyLock(&lock, initializationFunction); + // Try to suspend to handle a case when this thread doesn't call the initialization function. + mm::SuspendIfRequested(); + }); + } + waitUntilThreadsAreReady(); + + auto gcThread = std::async(std::launch::async, [] { + mm::RequestThreadsSuspension(); + mm::WaitForThreadsSuspension(); + mm::ResumeThreads(); + }); + while(!mm::IsThreadSuspensionRequested()) { + } + canStart = true; + + auto futureStatus = gcThread.wait_for(std::chrono::seconds(10)); + EXPECT_NE(futureStatus, std::future_status::timeout); + if (futureStatus == std::future_status::timeout) { + // Possibly CallInitGlobalPossiblyLock is hanging in a dead-lock. + // Set the lock variable to FILE_INITIALIZED to interrupt the loop inside CallInitGlobalPossiblyLock. + // And wait for the gc thread to stop. + lock = internal::FILE_INITIALIZED; + gcThread.wait(); + } + + for (auto& t : threads) { + t.join(); + } +} \ No newline at end of file diff --git a/kotlin-native/runtime/tsan_suppressions.txt b/kotlin-native/runtime/tsan_suppressions.txt index 7805285b034..ee58e333fc1 100644 --- a/kotlin-native/runtime/tsan_suppressions.txt +++ b/kotlin-native/runtime/tsan_suppressions.txt @@ -1,3 +1,7 @@ # Trust mimalloc to be thread safe. race:^mi_ race:^_mi_ + +# Spin lock on a volatile int in initialization of globals +# TODO: Remove when volatile int is replaced with a C++ atomic. +race:CallInitGlobalPossiblyLock \ No newline at end of file