Skip to content

Commit 54ac493

Browse files
committed
Added KMP ThreadLocal
1 parent 126ec96 commit 54ac493

File tree

10 files changed

+353
-0
lines changed

10 files changed

+353
-0
lines changed

utils/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,10 @@ kotlin {
1818
implementation(libs.coroutines.core)
1919
}
2020
}
21+
commonTest {
22+
dependencies {
23+
implementation(kotlin("test"))
24+
}
25+
}
2126
}
2227
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.internal.utils.thread
6+
7+
import kotlinx.rpc.internal.utils.InternalRpcApi
8+
import kotlinx.rpc.internal.utils.map.RpcInternalConcurrentHashMap
9+
10+
/**
11+
* A cross-platform implementation of ThreadLocal for Kotlin Multiplatform.
12+
* This class provides thread-local variables, which are variables that are local to a thread.
13+
* Each thread that accesses a thread-local variable has its own, independently initialized copy of the variable.
14+
*/
15+
@InternalRpcApi
16+
public class RpcInternalThreadLocal<T : Any> {
17+
private val map = RpcInternalConcurrentHashMap<Long, T>()
18+
19+
/**
20+
* Returns the value of this thread-local variable for the current thread or null.
21+
*/
22+
public fun get(): T? {
23+
val threadId = currentThreadId()
24+
return map[threadId]
25+
}
26+
27+
/**
28+
* Sets the value of this thread-local variable for the current thread.
29+
*/
30+
public fun set(value: T) {
31+
val threadId = currentThreadId()
32+
map[threadId] = value
33+
}
34+
35+
/**
36+
* Removes the value of this thread-local variable for the current thread.
37+
*/
38+
public fun remove() {
39+
val threadId = currentThreadId()
40+
map.remove(threadId)
41+
}
42+
}
43+
44+
/**
45+
* Returns the current thread's ID.
46+
* This is a platform-specific function that must be implemented for each platform.
47+
*/
48+
internal expect fun currentThreadId(): Long
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.internal.utils.thread
6+
7+
import kotlin.test.Test
8+
import kotlin.test.assertEquals
9+
10+
class ThreadLocalTest {
11+
@Test
12+
fun testGetWithInitialValue() {
13+
val threadLocal = RpcInternalThreadLocal<String>()
14+
val value = threadLocal.get()
15+
assertEquals(null, value)
16+
}
17+
18+
@Test
19+
fun testSetAndGet() {
20+
val threadLocal = RpcInternalThreadLocal<Int>()
21+
threadLocal.set(42)
22+
val value = threadLocal.get()
23+
assertEquals(42, value)
24+
}
25+
26+
@Test
27+
fun testRemove() {
28+
val threadLocal = RpcInternalThreadLocal<String>()
29+
threadLocal.set("value")
30+
assertEquals("value", threadLocal.get())
31+
32+
threadLocal.remove()
33+
assertEquals(null, threadLocal.get())
34+
}
35+
36+
@Test
37+
fun testMultipleThreadLocals() {
38+
val threadLocal1 = RpcInternalThreadLocal<String>()
39+
val threadLocal2 = RpcInternalThreadLocal<Int>()
40+
41+
threadLocal1.set("value1")
42+
threadLocal2.set(42)
43+
44+
assertEquals("value1", threadLocal1.get())
45+
assertEquals(42, threadLocal2.get())
46+
}
47+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.internal.utils.thread
6+
7+
import kotlinx.rpc.internal.utils.InternalRpcApi
8+
9+
/**
10+
* JS implementation of currentThreadId.
11+
* Since JavaScript is single-threaded (except for Web Workers), we use a global counter
12+
* to simulate thread IDs. In a real multi-threaded environment with Web Workers,
13+
* a more sophisticated approach would be needed.
14+
*/
15+
@InternalRpcApi
16+
public actual fun currentThreadId(): Long = 1L // Always return 1 for main thread in JS
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.internal.utils.thread
6+
7+
import kotlinx.rpc.internal.utils.InternalRpcApi
8+
9+
/**
10+
* JVM implementation of currentThreadId that returns the current thread's ID.
11+
*/
12+
@InternalRpcApi
13+
public actual fun currentThreadId(): Long = Thread.currentThread().id
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.internal.utils.thread
6+
7+
import kotlin.test.Test
8+
import kotlin.test.assertEquals
9+
import java.util.concurrent.CountDownLatch
10+
import kotlin.concurrent.thread
11+
12+
class ThreadLocalJvmTest {
13+
@Test
14+
fun testThreadLocalInMultipleThreads() {
15+
val threadLocal = RpcInternalThreadLocal<String>()
16+
val mainThreadId = Thread.currentThread().id
17+
18+
// Set a value in the main thread
19+
threadLocal.set("main-thread-value")
20+
21+
// Create a new thread and verify it has its own value
22+
val latch = CountDownLatch(1)
23+
var threadIdRef: Long = -1
24+
var threadValueRef: String? = null
25+
26+
thread {
27+
try {
28+
// This thread should have a different thread ID
29+
threadIdRef = Thread.currentThread().id
30+
31+
// The thread local should return the default value since we haven't set it in this thread
32+
threadValueRef = threadLocal.get()
33+
34+
// Now set a different value in this thread
35+
threadLocal.set("worker-thread-value")
36+
} finally {
37+
latch.countDown()
38+
}
39+
}
40+
41+
// Wait for the thread to complete
42+
latch.await()
43+
44+
// Verify the main thread's value is still intact
45+
assertEquals("main-thread-value", threadLocal.get())
46+
47+
// Verify the worker thread had a different ID
48+
assert(threadIdRef != mainThreadId) { "Worker thread should have a different ID" }
49+
50+
// Verify the worker thread initially got the default value
51+
assertEquals(null, threadValueRef)
52+
53+
// Create another thread to verify the worker thread's value
54+
val latch2 = CountDownLatch(1)
55+
var threadValue2Ref: String? = null
56+
57+
thread {
58+
try {
59+
// This should be a new thread with its own value
60+
threadValue2Ref = threadLocal.get()
61+
} finally {
62+
latch2.countDown()
63+
}
64+
}
65+
66+
latch2.await()
67+
68+
// Verify the second thread got its own default value
69+
assertEquals(null, threadValue2Ref)
70+
}
71+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.internal.utils.thread
6+
7+
import kotlinx.rpc.internal.utils.InternalRpcApi
8+
import kotlin.native.concurrent.ObsoleteWorkersApi
9+
import kotlin.native.concurrent.Worker
10+
11+
/**
12+
* Native implementation of currentThreadId that returns the current worker's ID.
13+
* In Kotlin/Native, we use Worker.current.id as a thread identifier.
14+
*/
15+
@OptIn(ObsoleteWorkersApi::class)
16+
@InternalRpcApi
17+
public actual fun currentThreadId(): Long = Worker.current.id.toLong()
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.internal.utils.thread
6+
7+
import kotlin.test.Test
8+
import kotlin.test.assertEquals
9+
import kotlin.native.concurrent.ObsoleteWorkersApi
10+
import kotlin.native.concurrent.Worker
11+
import kotlin.native.concurrent.Future
12+
import kotlin.native.concurrent.TransferMode
13+
import kotlin.experimental.ExperimentalNativeApi
14+
import kotlin.native.concurrent.withWorker
15+
16+
class ThreadLocalNativeTest {
17+
@OptIn(ObsoleteWorkersApi::class, ExperimentalNativeApi::class)
18+
@Test
19+
fun testThreadLocalInWorkers() {
20+
val threadLocal = RpcInternalThreadLocal<String>()
21+
val mainWorkerId = Worker.current.id.toLong()
22+
23+
// Set a value in the main worker
24+
threadLocal.set("main-worker-value")
25+
26+
// Create a new worker and verify it has its own value
27+
val worker = Worker.start()
28+
29+
// Execute a task in the worker
30+
val future = worker.execute(TransferMode.SAFE, { threadLocal }) { tl ->
31+
// Get the worker's ID
32+
val workerId = Worker.current.id.toLong()
33+
34+
// Get the initial value (should be null since we haven't set it in this worker)
35+
val initialValue = tl.get()
36+
37+
// Set a new value in this worker
38+
tl.set("worker-value")
39+
40+
// Return the worker ID and initial value
41+
Pair(workerId, initialValue)
42+
}
43+
44+
// Wait for the worker to complete and get the results
45+
val (workerId, workerInitialValue) = future.result
46+
47+
// Verify the worker had a different ID
48+
assert(workerId != mainWorkerId) { "Worker should have a different ID" }
49+
50+
// Verify the worker initially got null (default value)
51+
assertEquals(null, workerInitialValue)
52+
53+
// Verify the main worker's value is still intact
54+
assertEquals("main-worker-value", threadLocal.get())
55+
56+
// Create another worker to verify isolation
57+
val worker2 = Worker.start()
58+
59+
val future2 = worker2.execute(TransferMode.SAFE, { threadLocal }) { tl ->
60+
// This should be a new worker with its own value
61+
tl.get()
62+
}
63+
64+
// Verify the second worker got null (default value)
65+
assertEquals(null, future2.result)
66+
67+
// Clean up
68+
worker.requestTermination().result
69+
worker2.requestTermination().result
70+
}
71+
72+
@OptIn(ObsoleteWorkersApi::class, ExperimentalNativeApi::class)
73+
@Test
74+
fun testMultipleThreadLocalsInWorkers() {
75+
val threadLocal1 = RpcInternalThreadLocal<String>()
76+
val threadLocal2 = RpcInternalThreadLocal<Int>()
77+
78+
// Set values in the main worker
79+
threadLocal1.set("main-value")
80+
threadLocal2.set(42)
81+
82+
// Create a worker and set different values
83+
val worker = Worker.start()
84+
85+
val future = worker.execute(TransferMode.SAFE, { Pair(threadLocal1, threadLocal2) }) { (tl1, tl2) ->
86+
// Set different values in the worker
87+
tl1.set("worker-value")
88+
tl2.set(99)
89+
90+
// Return the values
91+
Pair(tl1.get(), tl2.get())
92+
}
93+
94+
// Get the worker's values
95+
val (workerValue1, workerValue2) = future.result
96+
97+
// Verify the worker's values
98+
assertEquals("worker-value", workerValue1)
99+
assertEquals(99, workerValue2)
100+
101+
// Verify the main worker's values are unchanged
102+
assertEquals("main-value", threadLocal1.get())
103+
assertEquals(42, threadLocal2.get())
104+
105+
// Clean up
106+
worker.requestTermination().result
107+
}
108+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.internal.utils.thread
6+
7+
import kotlinx.rpc.internal.utils.InternalRpcApi
8+
9+
/**
10+
* WASM JS implementation of currentThreadId.
11+
* Since WASM JS is single-threaded (similar to regular JS), we return a constant value.
12+
*/
13+
@InternalRpcApi
14+
public actual fun currentThreadId(): Long = 1L // Always return 1 for main thread in WASM JS
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.internal.utils.thread
6+
7+
import kotlinx.rpc.internal.utils.InternalRpcApi
8+
9+
/**
10+
* WASM WASI implementation of currentThreadId.
11+
* Since WASM WASI is single-threaded in the current implementation, we return a constant value.
12+
*/
13+
@InternalRpcApi
14+
public actual fun currentThreadId(): Long = 1L // Always return 1 for main thread in WASM WASI

0 commit comments

Comments
 (0)