[K/N][Runtime] Fix deadlock in file initialization

Issue #KT-50491 Fixed
This commit is contained in:
Ilya Matveev
2021-12-22 19:40:18 +07:00
committed by Space
parent fd63fed8b4
commit 2f32214106
4 changed files with 103 additions and 7 deletions
+14 -7
View File
@@ -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