Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider discouraging CoroutineStart.LAZY #4202

Open
dkhalanskyjb opened this issue Aug 1, 2024 · 1 comment
Open

Consider discouraging CoroutineStart.LAZY #4202

dkhalanskyjb opened this issue Aug 1, 2024 · 1 comment

Comments

@dkhalanskyjb
Copy link
Collaborator

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.

@tianyu
Copy link

tianyu commented Oct 11, 2024

I completely agree. I've been bitten more than once by a coroutineScope refusing to return because an async(start = LAZY) was never started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants