-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Introduce lazyAsync #4423
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
Comments
This is basically a variant of memoization, and if considered for inclusion I'd prefer a timeout parameter also be introduced (which can default to infinity). I'm surprised there isn't a memoization feature request already. I, too, wrote one a while ago (sans context param): https://github.com/JakeWharton/SdkSearch/blob/33e4b60af539aef108d1d3193f99ae151ce14733/backend/dac-proxy/src/main/java/com/jakewharton/sdksearch/proxy/memoize.kt. |
(additionally, if this is deemed useful, I can submit the PR.) |
This looks like a specialized version of https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/state-in.html, for which Jake's request for the timeout parameter is fulfilled by https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-sharing-started/-companion/-while-subscribed.html . The notable difference is that for
class MyRepository(
private val database: Database
) {
val indexes = lazyAsync {
withContext(NonCancellable) {
database.ensureIndex("a") { … }
database.ensureIndex("b") { … }
}
}
suspend fun findOneById(id: String) {
indexes.await()
// …
}
} Here, let's call Some other thoughts:
|
Users can ensure a specific context either by using
The name 'additional' doesn't appear in the public API. The Making the context mandatory defeats the entire purpose of this feature: declaring code to run later. With a mandatory context, this is basically just
Indeed, this is not the behavior of my implementation. I agree that it would be a better if it behaved the way you described, but that is a matter of writing a better implementation, I don't think this is a rebuttal of the feature itself.
Yes, this is on purpose.
Uh, I never realized you could semi-initialize a
Doesn't the lock guarantee that it does? Anyway—as I mentioned, the implementation isn't normative, it was just a quick draft to demonstrate the intent. If anything, the number of flaws you found in my implementation is probably a sign that this should be provided by the Coroutines authors directly and not left to users to implement for themselves… As mentioned by both Jake and I, there are multiple implementations in the wild already. |
Sure, this is fixable, my point is that it looks like a foot gun.
The other coroutine builders do not inherit the context of the caller. Calling a
How do you propose this could be fixed in a better implementation? I think the issue is fundamental. Cancellation is cooperative, so if the code doesn't notice cancellation exceptions, the thread running that code gets stuck. It is only possible to return from
Nope. The rules for establishing when a CPU sees which changes another CPU has made are called "happens-before", and actions do not happen-before setting a non-volatile variable (that is, Though this, indeed, is just a bug and not a critical issue.
If this is an error-prone pattern, including it will only increase the chance that someone writes buggy code because of it instead of having to reach for the more appropriate tools (like |
Can you clarify how // The method we want to cache
suspend fun foo() { … }
// Declaration-time, no access to a CoroutineScope
val fooCached = …?
// Usage-time, some kind of .await() or suspend invoke or whatever to access the value
suspend fun bar() {
println("Cached: ${fooCached.await()}")
}
runBlocking {
repeat {
bar()
}
} |
The way the issue is stated, it doesn't: if you can't use any Searching around, I found further examples of attempts to implement this:
So, the need for this is obvious, and the analogy with |
So something like, fun <T> lazyAsync(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> Unit,
): Deferred<T> = LazyDeferredImpl(coroutineContext, block)
private class LazyDeferredImpl(
private val additionalContext: CoroutineContext,
private val initializer: suspend CoroutineScope.() -> T,
) : Deferred<T> {
private val scope = CoroutineScope(SupervisorJob() + additionalContext)
private val deferred = scope.async(CoroutineStart.LAZY) { initialize() }
override suspend fun await(): T =
deferred.await()
// …
} Having a local scope seems a bit strange to me, but if you say it's a good pattern I don't particularly have an issue with it. I guess a difference is that it will not try to re-initialize itself if the first attempt fails/is cancelled, but I don't think that's a deal-breaker (I didn't even know my version did this). A big difference, however, is that this version will not be able to access the coroutine context of the running application. While that's a double-edged sword (it's easy to create a lazy that has a different value based on the call-site order), I think there is value in being able to access the context from there. |
Do you have some specific examples in mind? |
@dkhalanskyjb I thought about it and reviewed my existing usages, and I can't manage to convince myself either way.
|
Use case
I often happen to want to declare a suspending computation meant to be executed later, but do not have access to a scope at that moment.
If I had access to a scope, I could use
async(start = LAZY)
(or a remplacement as described in #4202). However, in these situations, I do not have access to a scope.As a first example, see this thread in KotlinLang. The user is attempting to initialize a data structure, which requires an async operation. However, that async operation doesn't need to happen right now, it could happen on first use. A simplified version of the problem is:
This example could be rewritten:
Another example can be found in the declaration of database indexes or other such metadata. It would be great to be able to write:
With this proposal, this example could be rewritten as:
Over the years I've seen many examples that could be boiled down to either of the two scenarii described above.
The Shape of the API
Simple implementation:
I'm sure there are many ways to optimize the implementation, this one is merely an example. Looking at the existing codebase, probably null-ing the
initializer
after it has run, using some kind of atomic reference for thevalue
with aval UNINITIALIZED = Any()
special value instead of having aMutex
, probably?I don't have a particular attachment to the name
lazyAsync
. Maybe another name can be more useful, I don't know.Prior Art
The
Shared
concept from the Prepared library (I'm the author) is essentially the same thing. AShared
value is a test fixture that is declared once globally, and can be used within multiple tests; its initializer issuspend
and only runs the first time the shared value is awaited, after which its result is cached for all further usages.This class allows declaring suspending test fixtures (e.g.
shared { MongoClient.connect() }
) and reusing them between many tests (even though they have differentCoroutineContext
which we cannot access at declaration-time) with the guarantee that it will only be initialized once.The text was updated successfully, but these errors were encountered: