[K/N][Runtime] Fix deadlock in file initialization
Issue #KT-50491 Fixed
This commit is contained in:
@@ -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<kotlin::ThreadState::kRunnable>(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<kotlin::ThreadState::kRunnable>(ThrowFileFailedToInitializeException);
|
||||
} while (localState != FILE_INITIALIZED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -4,14 +4,18 @@
|
||||
*/
|
||||
|
||||
#include "MemoryPrivate.hpp"
|
||||
#include "Runtime.h"
|
||||
#include "RuntimePrivate.hpp"
|
||||
#include "ThreadSuspension.hpp"
|
||||
#include "ThreadState.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include <future>
|
||||
#include <thread>
|
||||
#include <TestSupport.hpp>
|
||||
#include <TestSupportCompilerGenerated.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
@@ -55,6 +59,17 @@ void reportProgress(size_t currentIteration, size_t totalIterations) {
|
||||
}
|
||||
}
|
||||
|
||||
testing::MockFunction<void()>* initializationMock = nullptr;
|
||||
|
||||
void initializationFunction() {
|
||||
ASSERT_NE(initializationMock, nullptr);
|
||||
initializationMock->Call();
|
||||
}
|
||||
|
||||
test_support::ScopedMockFunction<void(), /* Strict = */ true> 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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user