🚀 Exploring Thread-Safe Lazy Initialization with Coroutines: LazySuspend Comes into Play

🤔 Problem Statement
In October 2018, a GitHub user proposed introducing a suspending version of Kotlin’s lazy { ... }
function to handle expensive initializations without blocking threads. While lazy
effectively defers initialization until needed, it can still block execution, making it less suitable for coroutine-based, non-blocking applications. To solve this, contributors suggested using async(start = LAZY)
, allowing initialization to be deferred and executed asynchronously on first access. Several custom coroutine-based implementations emerged to bridge this gap, but despite strong interest, the feature was never integrated into the standard Kotlin library.
As of now, the Kotlin standard library does not include a built-in suspending version of the lazy
function. The discussion on GitHub Issue #706 concluded without integrating this feature into the library. In the meantime, developers have explored alternative approaches, such as Mr Roman Elizarov, from his gist
📣 📣 📣 Everyone finding this gist via Google! Modern
kotlinx.coroutines
has out-of-the-box support forasyncLazy
with the following expression:val myLazyValue = async(start = CoroutineStart.LAZY) { ... }
. UsemyLazyValue.await()
when you need it.
as well as lazily-started-async. However, it's important to note that the use of CoroutineStart.LAZY
has been debated within the community. A recent discussion in GitHub Issue #4147 considered discouraging its use due to potential complexities and difficulties in code readability.
Given these considerations, if you require suspending lazy initialization, you might opt for a custom implementation tailored to your specific use case. So, how can we implement true non-blocking lazy initialization in coroutines?
Let’s explore some practical solutions. 🚀
- 🎯 Introduction
- 🔥 Implementation
- 🏆 Conclusion
- 🌐 References
🎯 Introduction
Lazy initialization is a powerful pattern that delays object creation until it’s actually needed, improving performance and resource management. But what if you need to initialize a value asynchronously inside Kotlin coroutines? That’s where LazySuspend
comes in! 🌟
🛠 Why Do We Need LazySuspend
?
Kotlin provides lazy {}
for synchronous lazy initialization, but it does not support suspending functions. Imagine you need to load data from a database or fetch an API response asynchronously. 🤯 Consider this example:
val storageProvider by lazy {
initializeStorageProvider() // Cannot be a suspend function 😢
}
suspend fun initializeStorageProvider(){
// ... long-running task
}
This won’t work if initializeStorageProvider
is a suspend
function! Instead, we need a coroutine-friendly lazy initialization mechanism. 💡
🔥 Implementation
1️⃣ Approach 1
import kotlinx.coroutines.*
import kotlin.coroutines.*
class LazySuspend<T>(private val initializer: suspend () -> T) {
@Volatile
private var cachedValue: T? = null
private val mutex = Mutex()
suspend fun getValue(): T {
if (cachedValue != null) return cachedValue!!
return mutex.withLock {
if (cachedValue == null) {
cachedValue = initializer()
}
cachedValue!!
}
}
}
- ✅ Uses a suspending function for initialization.
- ✅ Uses a mutex (
withLock
) to ensure thread safety (prevents race conditions in multithreading). - ✅ Stores the computed value after the first call, so subsequent calls return instantly.
suspend fun main() {
val lazyValue = LazySuspend {
println("Initializing...")
delay(1000) // Simulate long computation
"Hello, Coroutine Lazy!"
}
println("Before accessing value...")
println("Value: ${lazyValue.getValue()}") // Triggers initialization
println("Value again: ${lazyValue.getValue()}") // Uses cached value
}
// output
Before accessing value...
Initializing...
Value: Hello, Coroutine Lazy!
Value again: Hello, Coroutine Lazy!
2️⃣ Approach 2: Deferred
class LazySuspendDeferred<T>(scope: CoroutineScope, initializer: suspend () -> T) {
private val deferred = scope.async(start = CoroutineStart.LAZY) { initializer() }
suspend fun getValue(): T = deferred.await()
}
3️⃣ Approach 3: SuspendLazy from kt.academy
This function allows deferred execution of a block of code that is initialized only once in a coroutine, similar to lazy initialization. It ensures thread safety by using a Mutex
and provides mechanisms to handle initialization failures and context propagation, for further details, visit the original article.
4️⃣ Approach 4: LazySuspend from ME ✌️😊
Why I choose LazySuspend instead of SuspendLazy?
Both LazySuspend and SuspendLazy are reasonable names, but the better choice depends on readability, consistency, and convention.
- Matches the existing
lazy { ... }
function in Kotlin. - Emphasizes “lazy” behavior first, making it clear this is an alternative to
lazy { ... }
. - Easier to recognize for Kotlin developers already familiar with
lazy
.
The approach 3 may have some ✨ potential improvements, so that’s why I come up with LazySuspend
✅ Avoid Unsafe Casts: The code currently casts holder
to T
, which might cause issues if holder
was never properly assigned. Instead, you can use a sealed class or an AtomicReference
.
🔐 Ensuring Thread-Safety with Mutex
we ensure that only one coroutine initializes the value at a time, preventing race conditions. 🏎💨
⚠️ Handling Exceptions Gracefully: If the initializer
fails, holder
remains Any?
, causing an unsafe cast, leading to a ClassCastException.
LazyState
Sealed Class: This class is used to represent the current state of a value, whether it’s uninitialized, initialized with a value, or failed due to an exception.LazySuspend
Interface: This interface extends a suspending function (suspend () -> T
) and adds additional properties and methods:isInitialized
: A boolean property to check if the value has been initialized.getOrNull()
: Returns the value if initialized, or null if not.invoke()
: The main suspending function to retrieve the lazily initialized value.lazySuspend
Function: This function creates an instance ofLazySuspend
that lazily initializes a value using the provided suspending function (initializer
). It uses atomic references for thread-safety and double-checked locking to ensure that the value is initialized only once.
How can I ensure that the
LazySuspend
initialization runs on a background thread instead of the main thread in Kotlin?
To ensure that the lazySuspend
initialization does not run on the main thread, you can explicitly use a different coroutine dispatcher when invoking the suspending function inside the initializer. You can use Dispatchers.IO
, Dispatchers.Default
, or any custom dispatcher to offload the work to a background thread. Here’s how you can modify your lazySuspend
initialization to run on a background thread:
import kotlinx.coroutines.*
val lazyValue = lazySuspend {
withContext(Dispatchers.IO) { // Ensure this runs on a background thread
println("Initialized on thread: ${Thread.currentThread().name}")
longRunningTask() // Simulate some background work
}
}
🚨 Disclaimers
Approach 4 may have some disadvantages:
- Complexity: The custom implementation adds complexity compared to Kotlin’s built-in
lazy
. - Potential Overhead: Double-checked locking may introduce unnecessary overhead in single-threaded scenarios.
- Limited Use Case: The extra control may not be required for simpler lazy initialization needs.
🏆 Conclusion
The LazySuspend
interface includes methods to check if the value is initialized (isInitialized
), retrieve it if available (getOrNull()
), and lazily initialize it when accessed (invoke()
). The LazySuspend
provides lazy, suspend-aware initialization while ensuring thread safety and error handling. 🚀 Whether you're fetching API data, caching results, or managing expensive computations, LazySuspend
is a powerful tool in your Kotlin tools.
Give it a try in your next project! 🛠️
// Step 1: Grab from Maven central at the coordinates:
repositories {
google()
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/nphausg/loomIn")
}
}
// Step 2: Implementation from your module
$latestVersion = "0.0.1-alpha"
implementation("com.nphausg:loom:$latestVersion")
🌐 References
- Kotlin Coroutines Documentation — Kotlinlang.org
- Mutex in Kotlin Coroutines — Kotlin Coroutines Guide
- Lazy Initialization in Kotlin — JetBrains Blog
- AtomicReference in Java — Java Documentation