Rework frozen lazy to explicitly prevent cyclic garbage. (#1894)

This commit is contained in:
Nikolay Igotti
2018-08-17 17:47:35 +03:00
committed by GitHub
parent 562057cf69
commit 66767ec3fd
5 changed files with 165 additions and 33 deletions
+6
View File
@@ -724,6 +724,12 @@ task lazy0(type: RunKonanTest) {
source = "runtime/workers/lazy0.kt"
}
task lazy1(type: RunKonanTest) {
disabled = (project.testTarget == 'wasm32') // Need exceptions.
goldValue = "OK\n"
source = "runtime/workers/lazy1.kt"
}
task enumIdentity(type: RunKonanTest) {
disabled = (project.testTarget == 'wasm32') // Workers need pthreads.
goldValue = "true\n"
@@ -0,0 +1,17 @@
package runtime.workers.lazy1
import kotlin.test.*
import kotlin.native.worker.*
class Leak {
val leak by lazy { this }
}
@Test fun runTest() {
assertFailsWith<InvalidMutabilityException> {
for (i in 1..100)
Leak().freeze().leak
}
println("OK")
}
+43 -1
View File
@@ -447,6 +447,7 @@ namespace {
template<typename func>
inline void traverseContainerObjectFields(ContainerHeader* container, func process) {
RuntimeAssert(!isAggregatingFrozenContainer(container), "Must not be called on such containers");
ObjHeader* obj = reinterpret_cast<ObjHeader*>(container + 1);
for (int object = 0; object < container->objectCount(); object++) {
const TypeInfo* typeInfo = obj->type_info();
@@ -1753,7 +1754,7 @@ OBJ_GETTER(ReadRefLocked, ObjHeader** location, int32_t* spinlock) {
return value;
}
void EnsureNeverFrozen(KRef object) {
void EnsureNeverFrozen(ObjHeader* object) {
if (object->container()->frozen())
ThrowFreezingException(object, object);
// TODO: note, that this API could not not be called on frozen objects, so no need to care much about concurrency,
@@ -1761,4 +1762,45 @@ void EnsureNeverFrozen(KRef object) {
object->meta_object()->flags_ |= MF_NEVER_FROZEN;
}
KBoolean Konan_ensureAcyclicAndSet(ObjHeader* where, KInt index, ObjHeader* what) {
RuntimeAssert(where->container()->frozen(), "Must be used on frozen objects only");
RuntimeAssert(what == nullptr || what->container()->permanentOrFrozen(),
"Must be used with an immutable value");
if (what != nullptr) {
// Now we check that `where` is not reachable from `what`.
// As we cannot modify objects while traversing, instead we remember all seen objects in a set.
KStdUnorderedSet<ContainerHeader*> seen;
KStdDeque<ContainerHeader*> queue;
queue.push_back(what->container());
bool acyclic = true;
while (!queue.empty() && acyclic) {
ContainerHeader* current = queue.front();
queue.pop_front();
seen.insert(current);
if (isAggregatingFrozenContainer(current)) {
ContainerHeader** subContainer = reinterpret_cast<ContainerHeader**>(current + 1);
for (int i = 0; i < current->objectCount(); ++i) {
if (seen.count(*subContainer) == 0)
queue.push_back(*subContainer++);
}
} else {
traverseContainerReferredObjects(current, [where, &queue, &acyclic, &seen](ObjHeader* obj) {
if (obj == where) {
acyclic = false;
} else {
auto* objContainer = obj->container();
if (seen.count(objContainer) == 0)
queue.push_back(objContainer);
}
});
}
}
if (!acyclic) return false;
}
UpdateRef(reinterpret_cast<ObjHeader**>(
reinterpret_cast<uintptr_t>(where + 1) + where->type_info()->objOffsets_[index]), what);
// Fence on updated location?
return true;
}
} // extern "C"
@@ -16,49 +16,47 @@
package kotlin.native.worker
import kotlin.native.internal.NoReorderFields
@SymbolName("Konan_ensureAcyclicAndSet")
private external fun ensureAcyclicAndSet(where: Any, index: Int, what: Any?): Boolean
@NoReorderFields
internal class FreezeAwareLazyImpl<out T>(initializer: () -> T) : Lazy<T> {
private var initializer_: (() -> T)? = initializer
// IMPORTANT: due to simplified ensureAcyclicAndSet() semantics fields here must be ordered like this,
// as an ordinal is used to refer a field.
private var value_: Any? = UNINITIALIZED
// Objects are not frozen by default on single-threaded targets, shall they?
private val valueFrozen_ = AtomicReference<Any?>(UNINITIALIZED.freeze())
private var initializer_: (() -> T)? = initializer
private val lock_ = Lock()
override val value: T
get() {
var result: Any? = value_
if (isFrozen) {
if (result !== UNINITIALIZED) {
assert(result !== INITIALIZING)
@Suppress("UNCHECKED_CAST")
return result as T
}
// Barrier.
// Note that recursive lazy initializers will hang here.
do {
result = valueFrozen_.get()
} while (result === INITIALIZING)
if (result !== UNINITIALIZED) {
@Suppress("UNCHECKED_CAST")
return result as T
}
// TODO: maybe release initializer in frozen case.
if (valueFrozen_.compareAndSwap(UNINITIALIZED, INITIALIZING) === UNINITIALIZED) {
locked(lock_) {
var result: Any? = value_
if (result !== UNINITIALIZED) {
assert(result !== INITIALIZING)
@Suppress("UNCHECKED_CAST")
return result as T
}
// Set value_ to INITIALIZING.
ensureAcyclicAndSet(this, 0, INITIALIZING)
result = initializer_!!().freeze()
val old = valueFrozen_.compareAndSwap(INITIALIZING, result)
assert(old === INITIALIZING)
} else {
do {
result = valueFrozen_.get()
} while (result === INITIALIZING)
// Set value_.
if (!ensureAcyclicAndSet(this, 0, result)) {
throw InvalidMutabilityException("Setting cyclic data via lazy in $this: $result")
}
// Clear initializer_ reference.
ensureAcyclicAndSet(this, 1, null)
@Suppress("UNCHECKED_CAST")
return result as T
}
assert(result !== UNINITIALIZED && result !== INITIALIZING)
@Suppress("UNCHECKED_CAST")
return result as T
} else {
var result: Any? = value_
if (result === UNINITIALIZED) {
result = initializer_!!()
if (isFrozen)
throw InvalidMutabilityException(this)
throw InvalidMutabilityException("$this got frozen during lazy evaluation" )
value_ = result
initializer_ = null
}
@@ -68,7 +66,7 @@ internal class FreezeAwareLazyImpl<out T>(initializer: () -> T) : Lazy<T> {
}
// Racy!
override fun isInitialized(): Boolean = (value_ !== UNINITIALIZED) || (valueFrozen_.get() !== UNINITIALIZED)
override fun isInitialized(): Boolean = (value_ !== UNINITIALIZED) && (value_ !== INITIALIZING)
override fun toString(): String = if (isInitialized())
value.toString() else "Lazy value not initialized yet."
@@ -0,0 +1,69 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package kotlin.native.worker
import kotlin.native.internal.Frozen
@ThreadLocal
private object CurrentThread {
val id = Any().freeze()
}
@Frozen
internal class Lock {
private val locker_ = AtomicInt(0)
private val reenterCount_ = AtomicInt(0)
// TODO: make it properly reschedule instead of spinning.
fun lock() {
val lockData = CurrentThread.id.hashCode()
loop@ do {
val old = locker_.compareAndSwap(0, lockData)
when (old) {
lockData -> {
// Was locked by us already.
reenterCount_.increment()
break@loop
}
0 -> {
// We just got the lock.
assert(reenterCount_.get() == 0)
break@loop
}
}
} while (true)
}
fun unlock() {
if (reenterCount_.get() > 0) {
reenterCount_.decrement()
} else {
val lockData = CurrentThread.id.hashCode()
val old = locker_.compareAndSwap(lockData, 0)
assert(old == lockData)
}
}
}
internal inline fun <R> locked(lock: Lock, block: () -> R): R {
lock.lock()
try {
return block()
} finally {
lock.unlock()
}
}