Skip to content

Consider discouraging CoroutineStart.LAZY #4202

Open
@dkhalanskyjb

Description

@dkhalanskyjb

Copying from #4147 (comment):


How does one use LAZY? Typically, in a pattern like

// a property
val optionallyRequiredValue = scope.async(start = CoroutineStart.LAZY) {
    // ...
}

But I don't see how this is better than

val optionallyRequiredValue by lazy {
    scope.async {
        // ...
    }
}

The latter option doesn't spawn unnecessary new child coroutines (which could stop the scope from completing if you aren't careful).

Or let's say you have something similar to

val sendOnes = scope.launch(start = CoroutineStart.LAZY) {
    while (true) { onesChannel.send(1) }
}

Then, you have to write things like sendOnes.start(); channel.receive() throughout your codebase. But how is this better than something like the following?

val onesChannel by lazy {
    Channel<Int>().also {
        scope.launch {
            while (true) { onesChannel.send(1) }
        }
    }
}

I can sort of see the utility of LAZY when you have to combine on-demand computation with the ability to reinitialize the variable anew:

@Volatile // or @Synchronized
var latestResult: Deferred<Int> = computeResult()

private fun computeResult(): Deferred<Int> = scope.async(start = CoroutineStart.LAZY) {
    // ...
}

fun update() {
    // latestResult?.let { it.cancel() } // can't do that: the clients will throw
    latestResult = computeResult()
}

But this looks to me like a good fit for a hot Flow chain: mutable state in concurrent scenarios is tricky to get right, it's best avoided, not to mention that this allows for many parallel computeResult() computations, which can DDoS the system.

I'm really struggling to think of a use case where CoroutineStart.LAZY is actually the superior option. When I search grep.app for LAZY usages, most of them are completely unclear to me and seem to have been created by accident, like https://github.com/Baeldung/kotlin-tutorials/blob/f203e8fd8571b8dcc313fca04c72196bc4948649/spring-boot-kotlin/src/main/kotlin/com/baeldung/nonblockingcoroutines/controller/ProductControllerCoroutines.kt#L31-L44 Why is this LAZY if it's immediately started?

https://github.com/JetBrains/compose-multiplatform/blob/6aad20ec087f54df168ad7e7ce03c4ded710a93c/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/AsyncCache.kt could be a mistake, or it could actually be dictated by some requirement to avoid deadlocks if CoroutineDispatcher.dispatch calls getOrLoad. I'm not sure.


The existence of LAZY led people to writing code that's more difficult to follow than the alternative without it. If LAZY really is a situationally useful thing at best, we could mark it as Delicate. If it is useless, we can deprecate it. For now, we can edit the docs, but in general, we should deliberately research how it's used in some larger codebases.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions