Skip to content

async lazy val pattern causes infinite suspend #745

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

Closed
antanas-arvasevicius opened this issue Oct 22, 2018 · 8 comments
Closed

async lazy val pattern causes infinite suspend #745

antanas-arvasevicius opened this issue Oct 22, 2018 · 8 comments
Labels

Comments

@antanas-arvasevicius
Copy link

antanas-arvasevicius commented Oct 22, 2018

Hi,
I've just noticed that using new structural coroutines, the code which uses async(start = CoroutineStart.LAZY) { } causes infinite suspend.

As I understand these async(start = CoroutineStart.LAZY) is now treated as a children of TestView object and new coroutine engine waits until these async completes. Shouldn't LAZY async coroutines be excluded from a wait list if they are not running?
Maybe there are more proper way to implement lazy async val?

test example:

class TestView(override val coroutineContext: CoroutineContext) : CoroutineScope {
    val title: Deferred<String> = async(start = CoroutineStart.LAZY) { "demo" }
    val description: Deferred<String> = async(start = CoroutineStart.LAZY) { "desc" }
}

class TestClass {
    suspend fun getView(id: Int): TestView = coroutineScope { TestView(coroutineContext = coroutineContext) }
}

runBlocking {
  val view = TestClass().getView(123)
}
@qwwdfsad
Copy link
Collaborator

As I understand these async(start = CoroutineStart.LAZY) is now treated as a children of TestView object and new coroutine engine waits until these async completes

Right.

Shouldn't LAZY async coroutines be excluded from a wait list if they are not running?

Not really, for a couple of reasons. Firstly, it will make scoping rules non-trivial. Instead of "All coroutines launched from the scope are part of this scope" it will be "If ..., otherwise ...". Mental model with a lot of corner cases are proven to be less understandable and more error-prone, and we definitely don't want that. Secondly, there will be data race by design between the first call to await and end of the wait list, which adds non-determinism to lazy values.

Maybe there are more proper way to implement lazy async val?

Depends on what you want. Both GlobalScope.async(start = LAZY) and by lazy { async {} } are fine, but have slightly different semantics.

@antanas-arvasevicius
Copy link
Author

Thanks for your response. Actually the problem is simple, we have some kind of "View" object which has many properties those are interdependent example:
Pseudo code:

class View(today:Date) { 
   val rawLogs = database.fetch(interval=today)
   val onlyAuthorizations = rawLogs.filter(it.type == Auth)
   val signatures = rawLog.filter(it.type == Signature)
   val signatureCount = signatures.length
}

Problem is that rawLogs is expensive operation and I don't want to call it unless it's needed, also I want to cache a value and run fetch only once per "View" object. It's like some kind of lazy report which evaluates when accessing object's property.
If these operations wouldn't be on coroutines when I just would use "by lazy", now I don't know. Maybe convert everything to suspend functions or something. Maybe there will be support for by lazy with coroutines?

@fvasco
Copy link
Contributor

fvasco commented Oct 22, 2018

Core library should include Deferred.getValue extension method.
We can consider by async as a good replacement of by lazy, and we should consider code like:

fun getLog() = coroutineScope {
  val logs by async(start = LAZY) { TODO() } // <-- GlobalScope?
  ...
}

@qwwdfsad
Copy link
Collaborator

qwwdfsad commented Nov 6, 2018

As I've mention previously, both GlobalScope.async(start = LAZY) and by lazy { async {} } are fine, depending on your cancellation pattern.
If rawLogs computation is pure IO and does not suspend internally, it is ok to use GlobalScope variant, otherwise by lazy may be more robust.

@qwwdfsad qwwdfsad closed this as completed Nov 6, 2018
@LouisCAD
Copy link
Contributor

When using by lazy { async {} }, which CoroutineScope are we supposed to use?

@LouisCAD
Copy link
Contributor

My current code is the following, does it look right to you?

private class SuspendLazySuspendingImpl<out T>(
    private val dispatcher: CoroutineDispatcher = Dispatchers.Default,
    initializer: suspend () -> T
) {
    private val lazyValue = lazy { GlobalScope.async(dispatcher) { initializer() }}
    suspend operator fun invoke(): T = with(lazyValue) {
        if (isInitialized()) value else withContext(dispatcher) { value }
    }.await()
}

@chrisjenx
Copy link

chrisjenx commented Jul 3, 2020

Not sure why this is closed, this shouldn't break randomly depending on how you write the async.

This suspends forever:

withContext(Dispatchers.IO) {
 async(start= LAZY) {}
}

This works fine:

GlobalScope.async(Dispatchers.IO, start= LAZY) { }

There should not be magic like this - esp considering this has been a problem for nearly two years.

@fvasco
Copy link
Contributor

fvasco commented Jul 4, 2020

Hi, @chrisjenx,

You can consider #1065

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

No branches or pull requests

5 participants