From d3d8c8040def1268d0d4124ccc634b8932a2e34a Mon Sep 17 00:00:00 2001 From: Alexander Shabalin Date: Mon, 3 Aug 2020 14:44:45 +0300 Subject: [PATCH] Make WorkerBoundReference initially unfrozen (#4288) --- .../concurrent/worker_bound_reference0.kt | 242 ++++++++++++++---- runtime/src/main/cpp/Memory.cpp | 48 +++- runtime/src/main/cpp/WorkerBoundReference.cpp | 15 +- runtime/src/main/cpp/WorkerBoundReference.h | 2 + .../native/concurrent/WorkerBoundReference.kt | 23 +- 5 files changed, 265 insertions(+), 65 deletions(-) diff --git a/backend.native/tests/runtime/concurrent/worker_bound_reference0.kt b/backend.native/tests/runtime/concurrent/worker_bound_reference0.kt index f090c776f08..108ab0a5a43 100644 --- a/backend.native/tests/runtime/concurrent/worker_bound_reference0.kt +++ b/backend.native/tests/runtime/concurrent/worker_bound_reference0.kt @@ -14,6 +14,7 @@ import kotlin.text.Regex class A(var a: Int) +@SharedImmutable val global1: WorkerBoundReference = WorkerBoundReference(A(3)) @Test @@ -32,6 +33,7 @@ fun testGlobal() { worker.requestTermination().result } +@SharedImmutable val global2: WorkerBoundReference = WorkerBoundReference(A(3)) @Test @@ -52,6 +54,7 @@ fun testGlobalDenyAccessOnWorker() { worker.requestTermination().result } +@SharedImmutable val global3: WorkerBoundReference = WorkerBoundReference(A(3).freeze()) @Test @@ -69,6 +72,7 @@ fun testGlobalAccessOnWorkerFrozenInitially() { worker.requestTermination().result } +@SharedImmutable val global4: WorkerBoundReference = WorkerBoundReference(A(3)) @Test @@ -86,6 +90,7 @@ fun testGlobalAccessOnWorkerFrozenBeforePassing() { worker.requestTermination().result } +@SharedImmutable val global5: WorkerBoundReference = WorkerBoundReference(A(3)) @Test @@ -113,6 +118,7 @@ fun testGlobalAccessOnWorkerFrozenBeforeAccess() { worker.requestTermination().result } +@SharedImmutable val global6: WorkerBoundReference = WorkerBoundReference(A(3)) @Test @@ -140,6 +146,7 @@ fun testGlobalModification() { worker.requestTermination().result } +@SharedImmutable val global7: WorkerBoundReference = WorkerBoundReference(A(3)) @Test @@ -162,6 +169,13 @@ fun testLocal() { val local = WorkerBoundReference(A(3)) assertEquals(3, local.value.a) assertEquals(3, local.valueOrNull?.a) +} + +@Test +fun testLocalFrozen() { + val local = WorkerBoundReference(A(3)).freeze() + assertEquals(3, local.value.a) + assertEquals(3, local.valueOrNull?.a) val worker = Worker.start() val future = worker.execute(TransferMode.SAFE, { local }) { local -> @@ -175,8 +189,8 @@ fun testLocal() { } @Test -fun testLocalDenyAccessOnWorker() { - val local = WorkerBoundReference(A(3)) +fun testLocalDenyAccessOnWorkerFrozen() { + val local = WorkerBoundReference(A(3)).freeze() assertEquals(3, local.value.a) val worker = Worker.start() @@ -193,8 +207,8 @@ fun testLocalDenyAccessOnWorker() { } @Test -fun testLocalAccessOnWorkerFrozenInitially() { - val local = WorkerBoundReference(A(3).freeze()) +fun testLocalAccessOnWorkerFrozenInitiallyFrozen() { + val local = WorkerBoundReference(A(3).freeze()).freeze() assertEquals(3, local.value.a) assertEquals(3, local.valueOrNull?.a) @@ -209,8 +223,8 @@ fun testLocalAccessOnWorkerFrozenInitially() { } @Test -fun testLocalAccessOnWorkerFrozenBeforePassing() { - val local = WorkerBoundReference(A(3)) +fun testLocalAccessOnWorkerFrozenBeforePassingFrozen() { + val local = WorkerBoundReference(A(3)).freeze() assertEquals(3, local.value.a) local.value.freeze() @@ -225,10 +239,10 @@ fun testLocalAccessOnWorkerFrozenBeforePassing() { } @Test -fun testLocalAccessOnWorkerFrozenBeforeAccess() { +fun testLocalAccessOnWorkerFrozenBeforeAccessFrozen() { val semaphore: AtomicInt = AtomicInt(0) - val local = WorkerBoundReference(A(3)) + val local = WorkerBoundReference(A(3)).freeze() assertEquals(3, local.value.a) val worker = Worker.start() @@ -250,14 +264,25 @@ fun testLocalAccessOnWorkerFrozenBeforeAccess() { worker.requestTermination().result } - @Test -fun testLocalDenyAccessOnMainThread() { +fun testLocalAccessOnMainThread() { val worker = Worker.start() val future = worker.execute(TransferMode.SAFE, {}) { WorkerBoundReference(A(3)) } + assertEquals(3, future.result.value.a) + + worker.requestTermination().result +} + +@Test +fun testLocalDenyAccessOnMainThreadFrozen() { + val worker = Worker.start() + val future = worker.execute(TransferMode.SAFE, {}) { + WorkerBoundReference(A(3)).freeze() + } + val value = future.result assertFailsWith { value.value @@ -268,10 +293,10 @@ fun testLocalDenyAccessOnMainThread() { } @Test -fun testLocalModification() { +fun testLocalModificationFrozen() { val semaphore: AtomicInt = AtomicInt(0) - val local = WorkerBoundReference(A(3)) + val local = WorkerBoundReference(A(3)).freeze() assertEquals(3, local.value.a) val worker = Worker.start() @@ -294,8 +319,8 @@ fun testLocalModification() { } @Test -fun testLocalGetWorker() { - val local = WorkerBoundReference(A(3)) +fun testLocalGetWorkerFrozen() { + val local = WorkerBoundReference(A(3)).freeze() val ownerId = Worker.current.id assertEquals(ownerId, local.worker.id) @@ -326,9 +351,60 @@ fun testLocalForeignGetWorker() { worker.requestTermination().result } -fun getOwnerAndWeaks(initial: Int): Triple?>, WeakReference>, WeakReference> { +@Test +fun testLocalForeignGetWorkerFrozen() { + val worker = Worker.start() + val ownerId = worker.id + val future = worker.execute(TransferMode.SAFE, { ownerId }) { ownerId -> + val local = WorkerBoundReference(A(3)).freeze() + assertEquals(ownerId, local.worker.id) + local + } + + val value = future.result + assertEquals(ownerId, value.worker.id) + + worker.requestTermination().result +} + +class Wrapper(val ref: WorkerBoundReference) + +@Test +fun testLocalWithWrapperFrozen() { + val local = Wrapper(WorkerBoundReference(A(3))).freeze() + assertEquals(3, local.ref.value.a) + + val worker = Worker.start() + val future = worker.execute(TransferMode.SAFE, { local }) { local -> + local + } + + val value = future.result + assertEquals(3, value.ref.value.a) + worker.requestTermination().result +} + +@Test +fun testLocalDenyAccessWithWrapperFrozen() { + val local = Wrapper(WorkerBoundReference(A(3))).freeze() + assertEquals(3, local.ref.value.a) + + val worker = Worker.start() + val future = worker.execute(TransferMode.SAFE, { local }) { local -> + assertFailsWith { + local.ref.value + } + assertEquals(null, local.ref.valueOrNull) + Unit + } + + future.result + worker.requestTermination().result +} + +fun getOwnerAndWeaks(initial: Int): Triple?>, WeakReference>, WeakReference> { val ref = WorkerBoundReference(A(initial)) - val refOwner: AtomicReference?> = AtomicReference(ref) + val refOwner: FreezableAtomicReference?> = FreezableAtomicReference(ref) val refWeak = WeakReference(ref) val refValueWeak = WeakReference(ref.value) @@ -347,8 +423,29 @@ fun testCollect() { assertNull(refValueWeak.value) } -fun collectInWorker(worker: Worker, semaphore: AtomicInt): Pair, Future> { - val (refOwner, _, refValueWeak) = getOwnerAndWeaks(3) +fun getOwnerAndWeaksFrozen(initial: Int): Triple?>, WeakReference>, WeakReference> { + val ref = WorkerBoundReference(A(initial)).freeze() + val refOwner: AtomicReference?> = AtomicReference(ref) + val refWeak = WeakReference(ref) + val refValueWeak = WeakReference(ref.value) + + return Triple(refOwner, refWeak, refValueWeak) +} + +@Test +fun testCollectFrozen() { + val (refOwner, refWeak, refValueWeak) = getOwnerAndWeaksFrozen(3) + + refOwner.value = null + GC.collect() + + // Last reference to WorkerBoundReference is gone, so it and it's referent are destroyed. + assertNull(refWeak.value) + assertNull(refValueWeak.value) +} + +fun collectInWorkerFrozen(worker: Worker, semaphore: AtomicInt): Pair, Future> { + val (refOwner, _, refValueWeak) = getOwnerAndWeaksFrozen(3) val future = worker.execute(TransferMode.SAFE, { Pair(refOwner, semaphore) }) { (refOwner, semaphore) -> semaphore.increment() @@ -370,25 +467,24 @@ fun collectInWorker(worker: Worker, semaphore: AtomicInt): Pair } @Test -fun testCollectInWorker() { +fun testCollectInWorkerFrozen() { val semaphore: AtomicInt = AtomicInt(0) val worker = Worker.start() - val (refValueWeak, future) = collectInWorker(worker, semaphore) + val (refValueWeak, future) = collectInWorkerFrozen(worker, semaphore) semaphore.increment() future.result // At this point WorkerBoundReference no longer has a reference, so it's referent is destroyed. - // WorkerBoundReference, so referent is kept alive. GC.collect() assertNull(refValueWeak.value) worker.requestTermination().result } -fun doNotCollectInWorker(worker: Worker, semaphore: AtomicInt): Future> { - val ref = WorkerBoundReference(A(3)) +fun doNotCollectInWorkerFrozen(worker: Worker, semaphore: AtomicInt): Future> { + val ref = WorkerBoundReference(A(3)).freeze() return worker.execute(TransferMode.SAFE, { Pair(ref, semaphore) }) { (ref, semaphore) -> semaphore.increment() @@ -401,12 +497,12 @@ fun doNotCollectInWorker(worker: Worker, semaphore: AtomicInt): Future) -fun createCyclicGarbage(): Triple?>, WeakReference, WeakReference> { +fun createCyclicGarbage(): Triple?>, WeakReference, WeakReference> { val ref1 = WorkerBoundReference(B1()) - val ref1Owner: AtomicReference?> = AtomicReference(ref1) + val ref1Owner: FreezableAtomicReference?> = FreezableAtomicReference(ref1) val ref1Weak = WeakReference(ref1.value) val ref2 = WorkerBoundReference(B2(ref1)) @@ -437,26 +533,50 @@ fun createCyclicGarbage(): Triple?>, We } @Test -fun doesNotCollectCyclicGarbage() { +fun collectCyclicGarbage() { val (ref1Owner, ref1Weak, ref2Weak) = createCyclicGarbage() ref1Owner.value = null GC.collect() + assertNull(ref1Weak.value) + assertNull(ref2Weak.value) +} + +fun createCyclicGarbageFrozen(): Triple?>, WeakReference, WeakReference> { + val ref1 = WorkerBoundReference(B1()).freeze() + val ref1Owner: AtomicReference?> = AtomicReference(ref1) + val ref1Weak = WeakReference(ref1.value) + + val ref2 = WorkerBoundReference(B2(ref1)).freeze() + val ref2Weak = WeakReference(ref2.value) + + ref1.value.b2 = ref2 + + return Triple(ref1Owner, ref1Weak, ref2Weak) +} + +@Test +fun doesNotCollectCyclicGarbageFrozen() { + val (ref1Owner, ref1Weak, ref2Weak) = createCyclicGarbageFrozen() + + ref1Owner.value = null + GC.collect() + // If these asserts fail, that means WorkerBoundReference managed to clean up cyclic garbage all by itself. assertNotNull(ref1Weak.value) assertNotNull(ref2Weak.value) } -fun createCrossThreadCyclicGarbage( +fun createCrossThreadCyclicGarbageFrozen( worker: Worker ): Triple?>, WeakReference, WeakReference> { - val ref1 = WorkerBoundReference(B1()) + val ref1 = WorkerBoundReference(B1()).freeze() val ref1Owner: AtomicReference?> = AtomicReference(ref1) val ref1Weak = WeakReference(ref1.value) val future = worker.execute(TransferMode.SAFE, { ref1 }) { ref1 -> - val ref2 = WorkerBoundReference(B2(ref1)) + val ref2 = WorkerBoundReference(B2(ref1)).freeze() Pair(ref2, WeakReference(ref2.value)) } val (ref2, ref2Weak) = future.result @@ -467,10 +587,10 @@ fun createCrossThreadCyclicGarbage( } @Test -fun doesNotCollectCrossThreadCyclicGarbage() { +fun doesNotCollectCrossThreadCyclicGarbageFrozen() { val worker = Worker.start() - val (ref1Owner, ref1Weak, ref2Weak) = createCrossThreadCyclicGarbage(worker) + val (ref1Owner, ref1Weak, ref2Weak) = createCrossThreadCyclicGarbageFrozen(worker) ref1Owner.value = null GC.collect() @@ -493,11 +613,11 @@ class C1 { data class C2(val c1: AtomicReference>) -fun createCyclicGarbageWithAtomics(): Triple?>, WeakReference, WeakReference> { - val ref1 = WorkerBoundReference(C1()) +fun createCyclicGarbageWithAtomicsFrozen(): Triple?>, WeakReference, WeakReference> { + val ref1 = WorkerBoundReference(C1()).freeze() val ref1Weak = WeakReference(ref1.value) - val ref2 = WorkerBoundReference(C2(AtomicReference(ref1))) + val ref2 = WorkerBoundReference(C2(AtomicReference(ref1))).freeze() val ref2Weak = WeakReference(ref2.value) ref1.value.c2 = AtomicReference(ref2) @@ -511,8 +631,8 @@ fun dispose(refOwner: AtomicReference?>) { } @Test -fun doesNotCollectCyclicGarbageWithAtomics() { - val (ref1Owner, ref1Weak, ref2Weak) = createCyclicGarbageWithAtomics() +fun doesNotCollectCyclicGarbageWithAtomicsFrozen() { + val (ref1Owner, ref1Weak, ref2Weak) = createCyclicGarbageWithAtomicsFrozen() ref1Owner.value = null GC.collect() @@ -523,8 +643,8 @@ fun doesNotCollectCyclicGarbageWithAtomics() { } @Test -fun collectCyclicGarbageWithAtomics() { - val (ref1Owner, ref1Weak, ref2Weak) = createCyclicGarbageWithAtomics() +fun collectCyclicGarbageWithAtomicsFrozen() { + val (ref1Owner, ref1Weak, ref2Weak) = createCyclicGarbageWithAtomicsFrozen() dispose(ref1Owner) GC.collect() @@ -533,14 +653,14 @@ fun collectCyclicGarbageWithAtomics() { assertNull(ref2Weak.value) } -fun createCrossThreadCyclicGarbageWithAtomics( +fun createCrossThreadCyclicGarbageWithAtomicsFrozen( worker: Worker ): Triple?>, WeakReference, WeakReference> { - val ref1 = WorkerBoundReference(C1()) + val ref1 = WorkerBoundReference(C1()).freeze() val ref1Weak = WeakReference(ref1.value) val future = worker.execute(TransferMode.SAFE, { ref1 }) { ref1 -> - val ref2 = WorkerBoundReference(C2(AtomicReference(ref1))) + val ref2 = WorkerBoundReference(C2(AtomicReference(ref1))).freeze() Pair(ref2, WeakReference(ref2.value)) } val (ref2, ref2Weak) = future.result @@ -551,10 +671,10 @@ fun createCrossThreadCyclicGarbageWithAtomics( } @Test -fun doesNotCollectCrossThreadCyclicGarbageWithAtomics() { +fun doesNotCollectCrossThreadCyclicGarbageWithAtomicsFrozen() { val worker = Worker.start() - val (ref1Owner, ref1Weak, ref2Weak) = createCrossThreadCyclicGarbageWithAtomics(worker) + val (ref1Owner, ref1Weak, ref2Weak) = createCrossThreadCyclicGarbageWithAtomicsFrozen(worker) ref1Owner.value = null GC.collect() @@ -568,10 +688,10 @@ fun doesNotCollectCrossThreadCyclicGarbageWithAtomics() { } @Test -fun collectCrossThreadCyclicGarbageWithAtomics() { +fun collectCrossThreadCyclicGarbageWithAtomicsFrozen() { val worker = Worker.start() - val (ref1Owner, ref1Weak, ref2Weak) = createCrossThreadCyclicGarbageWithAtomics(worker) + val (ref1Owner, ref1Weak, ref2Weak) = createCrossThreadCyclicGarbageWithAtomicsFrozen(worker) dispose(ref1Owner) // This marks C2 as gone on the main thread @@ -588,11 +708,11 @@ fun collectCrossThreadCyclicGarbageWithAtomics() { } @Test -fun concurrentAccess() { +fun concurrentAccessFrozen() { val workerCount = 10 val workerUnlocker = AtomicInt(0) - val ref = WorkerBoundReference(A(3)) + val ref = WorkerBoundReference(A(3)).freeze() assertEquals(3, ref.value.a) val workers = Array(workerCount) { @@ -621,10 +741,10 @@ fun concurrentAccess() { } @Test -fun testExceptionMessage() { +fun testExceptionMessageFrozen() { val worker = Worker.start() val future = worker.execute(TransferMode.SAFE, {}) { - WorkerBoundReference(A(3)) + WorkerBoundReference(A(3)).freeze() } val value = future.result @@ -638,3 +758,23 @@ fun testExceptionMessage() { worker.requestTermination().result } + +@Test +fun testDoubleFreeze() { + val ref = WorkerBoundReference(A(3)) + val wrapper = Wrapper(ref) + ref.freeze() + ref.freeze() + wrapper.freeze() +} + +@Test +fun testDoubleFreezeWithFreezeBlocker() { + val ref = WorkerBoundReference(A(3)) + val wrapper = Wrapper(ref) + wrapper.ensureNeverFrozen() + assertFailsWith { + wrapper.freeze() + } + ref.freeze() +} diff --git a/runtime/src/main/cpp/Memory.cpp b/runtime/src/main/cpp/Memory.cpp index a2ffc3e82cf..5f9945b4659 100644 --- a/runtime/src/main/cpp/Memory.cpp +++ b/runtime/src/main/cpp/Memory.cpp @@ -748,17 +748,23 @@ inline void traverseReferredObjects(ObjHeader* obj, func process) { } template -inline void traverseContainerObjectFields(ContainerHeader* container, func process) { +inline void traverseContainerObjects(ContainerHeader* container, func process) { RuntimeAssert(!isAggregatingFrozenContainer(container), "Must not be called on such containers"); ObjHeader* obj = reinterpret_cast(container + 1); - - for (int object = 0; object < container->objectCount(); object++) { - traverseObjectFields(obj, process); + for (int i = 0; i < container->objectCount(); ++i) { + process(obj); obj = reinterpret_cast( reinterpret_cast(obj) + objectSize(obj)); } } +template +inline void traverseContainerObjectFields(ContainerHeader* container, func process) { + traverseContainerObjects(container, [process](ObjHeader* obj) { + traverseObjectFields(obj, process); + }); +} + template inline void traverseContainerReferredObjects(ContainerHeader* container, func process) { traverseContainerObjectFields(container, [process](ObjHeader** location) { @@ -2582,6 +2588,34 @@ void freezeCyclic(ObjHeader* root, } } +// These hooks are only allowed to modify `obj` subgraph. +void runFreezeHooks(ObjHeader* obj) { + if (obj->type_info() == theWorkerBoundReferenceTypeInfo) { + WorkerBoundReferenceFreezeHook(obj); + } +} + +void runFreezeHooksRecursive(ObjHeader* root) { + KStdUnorderedSet seen; + KStdVector toVisit; + seen.insert(root); + toVisit.push_back(root); + while (!toVisit.empty()) { + KRef obj = toVisit.back(); + toVisit.pop_back(); + + runFreezeHooks(obj); + + traverseReferredObjects(obj, [&seen, &toVisit](ObjHeader* field) { + auto wasNotSeenYet = seen.insert(field).second; + // Only iterating on unseen objects which containers will get frozen by freezeCyclic or freezeAcyclic. + if (wasNotSeenYet && canFreeze(field->container())) { + toVisit.push_back(field); + } + }); + } +} + /** * Theory of operations. * @@ -2612,6 +2646,12 @@ void freezeSubgraph(ObjHeader* root) { ContainerHeader* rootContainer = root->container(); if (isPermanentOrFrozen(rootContainer)) return; + MEMORY_LOG("Run freeze hooks on subgraph of %p\n", root); + + // Note: Actual freezing can fail, but these hooks won't be undone, and moreover + // these hooks will run again on a repeated freezing attempt. + runFreezeHooksRecursive(root); + MEMORY_LOG("Freeze subgraph of %p\n", root) #if USE_GC diff --git a/runtime/src/main/cpp/WorkerBoundReference.cpp b/runtime/src/main/cpp/WorkerBoundReference.cpp index 2399f11e136..0012212b5d6 100644 --- a/runtime/src/main/cpp/WorkerBoundReference.cpp +++ b/runtime/src/main/cpp/WorkerBoundReference.cpp @@ -24,9 +24,18 @@ WorkerBoundReference* asWorkerBoundReference(KRef thiz) { RUNTIME_NOTHROW void DisposeWorkerBoundReference(KRef thiz) { // DisposeSharedRef is only called when all references to thiz are gone. - auto* holder = asWorkerBoundReference(thiz)->holder; - holder->dispose(); - konanDestructInstance(holder); + // Can be null if WorkerBoundReference wasn't frozen. + if (auto* holder = asWorkerBoundReference(thiz)->holder) { + holder->dispose(); + konanDestructInstance(holder); + } +} + +// Defined in WorkerBoundReference.kt +extern "C" void Kotlin_WorkerBoundReference_freezeHook(KRef thiz); + +RUNTIME_NOTHROW void WorkerBoundReferenceFreezeHook(KRef thiz) { + Kotlin_WorkerBoundReference_freezeHook(thiz); } extern "C" { diff --git a/runtime/src/main/cpp/WorkerBoundReference.h b/runtime/src/main/cpp/WorkerBoundReference.h index 1c950dec3f9..eff74e8c0ca 100644 --- a/runtime/src/main/cpp/WorkerBoundReference.h +++ b/runtime/src/main/cpp/WorkerBoundReference.h @@ -13,4 +13,6 @@ // all types. RUNTIME_NOTHROW void DisposeWorkerBoundReference(KRef thiz) NO_INLINE; +RUNTIME_NOTHROW void WorkerBoundReferenceFreezeHook(KRef thiz); + #endif // RUNTIME_SHAREDREF_H diff --git a/runtime/src/main/kotlin/kotlin/native/concurrent/WorkerBoundReference.kt b/runtime/src/main/kotlin/kotlin/native/concurrent/WorkerBoundReference.kt index f47378a4d74..ca6e15e88a0 100644 --- a/runtime/src/main/kotlin/kotlin/native/concurrent/WorkerBoundReference.kt +++ b/runtime/src/main/kotlin/kotlin/native/concurrent/WorkerBoundReference.kt @@ -17,22 +17,22 @@ external private fun derefWorkerBoundReference(ref: NativePtr): Any? external private fun describeWorkerBoundReference(ref: NativePtr): String /** - * A frozen shared reference to a Kotlin object. + * A shared reference to a Kotlin object that doesn't freeze the referred object when it gets frozen itself. * - * Can be safely passed between workers, but [value] can only be accessed on the worker [WorkerBoundReference] was created on, - * unless the referred object is frozen too. + * After freezing can be safely passed between workers, but [value] can only be accessed on + * the worker [WorkerBoundReference] was created on, unless the referred object is frozen too. * - * Note: Garbage collector currently cannot free any reference cycles with [WorkerBoundReference] in them. + * Note: Garbage collector currently cannot free any reference cycles with frozen [WorkerBoundReference] in them. * To resolve such cycles consider using [AtomicReference] which can be explicitly * nulled out. */ -@Frozen @NoReorderFields @ExportTypeInfo("theWorkerBoundReferenceTypeInfo") public class WorkerBoundReference(value: T) { - private val ptr = createWorkerBoundReference(value) + private var ptr = NativePtr.NULL private val ownerName = Worker.current.name + private var valueBeforeFreezing: T? = value private val valueDescription get() = describeWorkerBoundReference(ptr) @@ -48,10 +48,19 @@ public class WorkerBoundReference(value: T) { * The referenced value or null if referred object is not frozen and current worker is different from the one created [this]. */ val valueOrNull: T? - get() = @Suppress("UNCHECKED_CAST") (derefWorkerBoundReference(ptr) as T?) + get() = valueBeforeFreezing ?: @Suppress("UNCHECKED_CAST") (derefWorkerBoundReference(ptr) as T?) /** * Worker that [value] is bound to. */ val worker: Worker = Worker.current + + @ExportForCppRuntime("Kotlin_WorkerBoundReference_freezeHook") + private fun freezeHook() { + // If this hook was already run, do nothing. + if (valueBeforeFreezing == null) + return + ptr = createWorkerBoundReference(valueBeforeFreezing!!) + valueBeforeFreezing = null + } }