Repository: evant/kotlin-inject (kotlin)

Issue:
Allow mutlithreading on kotlin native
Right now components can only be accessed on the main thread due to the `LazyMap` implementation. Need to figure out how to allow multithreading like what's available on the jvm. The solution should also work with kotlin's [new memory model](https://blog.jetbrains.com/kotlin/2021/08/try-the-new-kotlin-native-memory-manager-development-preview/).

Pull request description:
Note: this relies on the new kotlin memory model.

Fixes #167

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index ce8b16b2..104b1260 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -28,9 +28,12 @@ jobs:
       - run:
           name: Install libtinfo5
           command: sudo apt update && sudo apt install -y libtinfo5
+      - run:
+          name: Build
+          command: ./gradlew assemble
       - run:
           name: Run Tests
-          command: ./gradlew check
+          command: ./gradlew check --continue
       - gradle/collect_test_results:
           test_results_path: build/test-results
           reports_path: build/reports
@@ -61,9 +64,12 @@ jobs:
           key: >-
             gradle-{{ checksum "/tmp/gradle_cache_seed" }}-{{ checksum
             ".circleci/config.yml" }}-macos
+      - run:
+          name: Build
+          command: ./gradlew assemble
       - run:
           name: Run Tests
-          command: ./gradlew checkApple
+          command: ./gradlew checkApple --continue
       - gradle/collect_test_results:
           test_results_path: build/test-results
           reports_path: build/reports
diff --git a/build-logic/src/main/kotlin/kotlin-inject.multiplatform.gradle.kts b/build-logic/src/main/kotlin/kotlin-inject.multiplatform.gradle.kts
index 68eed3f5..51165ed0 100644
--- a/build-logic/src/main/kotlin/kotlin-inject.multiplatform.gradle.kts
+++ b/build-logic/src/main/kotlin/kotlin-inject.multiplatform.gradle.kts
@@ -50,9 +50,7 @@ kotlin {
         }
         // Ensure xml test reports are generated
         val jvmTest by tasks.getting(Test::class) {
-            doFirst {
-                reports.junitXml.required.set(true)
-            }
+            reports.junitXml.required.set(true)
         }
     }
 }
diff --git a/gradle.properties b/gradle.properties
index e59a67f2..6dcd6529 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -2,4 +2,5 @@ org.gradle.parallel=true
 kotlin.mpp.stability.nowarn=true
 kotlin.mpp.enableGranularSourceSetsMetadata=true
 kotlin.native.ignoreDisabledTargets=true
+kotlin.native.binary.memoryModel=experimental
 org.gradle.jvmargs=-Xmx2048m
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index aeda3602..b75be20d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -16,6 +16,7 @@ room-compiler-processing-testing = { module = "androidx.room:room-compiler-proce
 kotlin-annotation-processing-embeddable = { module = "org.jetbrains.kotlin:kotlin-annotation-processing-embeddable", version.ref = "kotlin" }
 kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.5.2" }
 kotlinx-metadata-jvm = { module = "org.jetbrains.kotlinx:kotlinx-metadata-jvm", version = "0.3.0" }
+kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version = "0.16.3" }
 jdk-compiler = { module = "io.earcam.wrapped:jdk.compiler", version = "1.8.132" }
 assertk = { module = "com.willowtreeapps.assertk:assertk", version = "0.25" }
 burst-junit4 = { module = "com.squareup.burst:burst-junit4", version = "1.2.0" }
diff --git a/kotlin-inject-runtime/build.gradle.kts b/kotlin-inject-runtime/build.gradle.kts
index e840c2e8..c2545abd 100644
--- a/kotlin-inject-runtime/build.gradle.kts
+++ b/kotlin-inject-runtime/build.gradle.kts
@@ -2,4 +2,21 @@ plugins {
     id("kotlin-inject.multiplatform")
     id("kotlin-inject.detekt")
     id("kotlin-inject.publish")
+}
+
+kotlin {
+    sourceSets {
+        nativeMain {
+            dependencies {
+                implementation(libs.kotlinx.atomicfu)
+            }
+        }
+        commonTest {
+            dependencies {
+                implementation(kotlin("test"))
+                implementation(libs.kotlinx.coroutines)
+                implementation(libs.assertk)
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/kotlin-inject-runtime/src/jvmMain/kotlin/me/tatarka/inject/internal/LazyMap.kt b/kotlin-inject-runtime/src/jvmMain/kotlin/me/tatarka/inject/internal/LazyMap.kt
index fb136fdf..60a2ed24 100644
--- a/kotlin-inject-runtime/src/jvmMain/kotlin/me/tatarka/inject/internal/LazyMap.kt
+++ b/kotlin-inject-runtime/src/jvmMain/kotlin/me/tatarka/inject/internal/LazyMap.kt
@@ -1,7 +1,6 @@
 package me.tatarka.inject.internal
 
 import java.util.concurrent.ConcurrentHashMap
-import kotlin.reflect.KClass
 
 private val NULL = Any()
 
@@ -10,17 +9,17 @@ actual class LazyMap {
 
     actual fun <T> get(key: String, init: () -> T): T {
         val result = map[key]
-        if (result == null) {
+        return if (result == null) {
             synchronized(map) {
                 var result = map[key]
                 if (result == null) {
                     result = init() ?: NULL
                     map[key] = result
                 }
-                return coerceResult(result)
+                coerceResult(result)
             }
         } else {
-            return coerceResult(result)
+            coerceResult(result)
         }
     }
 
diff --git a/kotlin-inject-runtime/src/nativeMain/kotlin/me/tatarka/inject/internal/LazyMap.kt b/kotlin-inject-runtime/src/nativeMain/kotlin/me/tatarka/inject/internal/LazyMap.kt
index 35ca0464..f4e1d893 100644
--- a/kotlin-inject-runtime/src/nativeMain/kotlin/me/tatarka/inject/internal/LazyMap.kt
+++ b/kotlin-inject-runtime/src/nativeMain/kotlin/me/tatarka/inject/internal/LazyMap.kt
@@ -1,17 +1,21 @@
 package me.tatarka.inject.internal
 
+import kotlinx.atomicfu.locks.ReentrantLock
+import kotlinx.atomicfu.locks.withLock
+
 private val NULL = Any()
 
 actual class LazyMap {
     private val map = mutableMapOf<String, Any>()
+    private val lock = ReentrantLock()
 
     actual fun <T> get(key: String, init: () -> T): T {
-        val result = map[key]
-        return if (result == null) {
-            val result = init() ?: NULL
-            map[key] = result
-            coerceResult(result)
-        } else {
+        return lock.withLock {
+            var result = map[key]
+            if (result == null) {
+                result = init() ?: NULL
+                map[key] = result
+            }
             coerceResult(result)
         }
     }


Test patch:
diff --git a/kotlin-inject-runtime/src/commonTest/kotlin/me/tatarka/inject/internal/LazyMapTest.kt b/kotlin-inject-runtime/src/commonTest/kotlin/me/tatarka/inject/internal/LazyMapTest.kt
new file mode 100644
index 00000000..f45ce1b6
--- /dev/null
+++ b/kotlin-inject-runtime/src/commonTest/kotlin/me/tatarka/inject/internal/LazyMapTest.kt
@@ -0,0 +1,25 @@
+package me.tatarka.inject.internal
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.isSameAs
+import kotlin.test.Test
+
+class LazyMapTest {
+    @Test
+    fun caches_value_between() {
+        val lazyMap = LazyMap()
+        var calls = 0
+        val value1 = lazyMap.get("key") {
+            calls++
+            Any()
+        }
+        val value2 = lazyMap.get("key") {
+            calls++
+            Any()
+        }
+
+        assertThat(calls).isEqualTo(1)
+        assertThat(value1).isSameAs(value2)
+    }
+}
\ No newline at end of file
diff --git a/kotlin-inject-runtime/src/jvmTest/kotlin/me/tatarka/inject/internal/LazyMapTestJvm.kt b/kotlin-inject-runtime/src/jvmTest/kotlin/me/tatarka/inject/internal/LazyMapTestJvm.kt
new file mode 100644
index 00000000..8b4bf3a4
--- /dev/null
+++ b/kotlin-inject-runtime/src/jvmTest/kotlin/me/tatarka/inject/internal/LazyMapTestJvm.kt
@@ -0,0 +1,27 @@
+package me.tatarka.inject.internal
+
+import assertk.assertThat
+import assertk.assertions.each
+import assertk.assertions.isSameAs
+import java.util.concurrent.Callable
+import java.util.concurrent.Executors
+import kotlin.test.Test
+
+class LazyMapTestJvm {
+    @Test
+    fun caches_value_from_multiple_threads() {
+        val lazyMap = LazyMap()
+        val count = 100
+        val executors = Executors.newFixedThreadPool(count)
+        val results = executors.invokeAll(List(count) {
+            Callable {
+                lazyMap.get("test") { Any() }
+            }
+        }).map { it.get() }
+        executors.shutdown()
+
+        assertThat(results).each {
+            it.isSameAs(results.first())
+        }
+    }
+}
\ No newline at end of file
diff --git a/kotlin-inject-runtime/src/nativeTest/kotlin/me/tatarka/inject/internal/LazyMapTestNative.kt b/kotlin-inject-runtime/src/nativeTest/kotlin/me/tatarka/inject/internal/LazyMapTestNative.kt
new file mode 100644
index 00000000..a85dbb18
--- /dev/null
+++ b/kotlin-inject-runtime/src/nativeTest/kotlin/me/tatarka/inject/internal/LazyMapTestNative.kt
@@ -0,0 +1,26 @@
+package me.tatarka.inject.internal
+
+import assertk.assertThat
+import assertk.assertions.each
+import assertk.assertions.isSameAs
+import kotlin.native.concurrent.TransferMode
+import kotlin.native.concurrent.Worker
+import kotlin.test.Test
+
+class LazyMapTestNative {
+
+    @Test
+    fun caches_value_from_multiple_threads() {
+        val lazyMap = LazyMap()
+        val count = 100
+        val results = List(count) {
+            Worker.start().execute(TransferMode.SAFE, { lazyMap }) {
+                it.get("test") { Any() }
+            }
+        }.map { it.result }
+
+        assertThat(results).each {
+            it.isSameAs(results.first())
+        }
+    }
+}
\ No newline at end of file