https://kotlinlang.org logo
#coroutines
Title
# coroutines
m

Mark

11/16/2019, 7:03 AM
Idiomatic way to lazily instantiate AND initialise a class that should only ever be instantiated and initialised max once. At the moment I have:
Copy code
private lateinit var deferredFactory: Deferred<MyFactory>

suspend fun getMyFactory(): MyFactory = withContext(Dispatchers.Main) {
    if (::deferredFactory.isInitialized) {
        return@withContext deferredFactory.await()
    }
    val deferred = CompletableDeferred<MyFactory>().apply {
        deferredFactory = this
    }
    createFactory().also {
        deferred.complete(it)
    }
}

private suspend fun createFactory() = withContext(Dispatchers.Default) {
    // slow stuff
}

=== UPDATE ===
After all the comments, this is what we came up with:

private val deferredFactory = GlobalScope.async(start = CoroutineStart.LAZY) {
    createFactory()
}

suspend fun getFactoryOrNull() =
    try {
        deferredFactory.await()
    } catch (e: Exception) {
        //loge("unable to create factory", e)
        null
    }
o

octylFractal

11/16/2019, 7:07 AM
I think it would be a lot simpler to use a Mutex around the whole thing (or double-check idiom for slightly faster common case), rather than a deferred variable
a

Animesh Sahu

11/16/2019, 7:26 AM
Why don't use contracts?
o

octylFractal

11/16/2019, 7:27 AM
one point against contracts is that iirc they are still experimental, so it may not be a good choice for long-lived code
it also doesn't help with the actual logic of avoiding calling the function more than once, only ensures that it isn't (if contracts can even ensure that...)
m

Mark

11/16/2019, 8:17 AM
@octylFractal is it really that much simpler with a mutex and a double-check? Also, if the MyFactory is nullable then that means I’d need to use an AtomicReference whereas this way I can use Deferred<MyFactory?>
o

octylFractal

11/16/2019, 8:18 AM
I would find it odd that you would need a null-state here
m

Mark

11/16/2019, 8:26 AM
Null state for when a feature is unavailable
o

octylFractal

11/16/2019, 8:28 AM
you're always free to split out another variable for checking
isInitialized
. I would prefer having a null object in this case ( https://en.wikipedia.org/wiki/Null_object_pattern )
m

Mark

11/16/2019, 8:33 AM
ok thanks. I made a slight modification since the
withContext(Dispatchers.Main)
is not really needed https://pl.kotl.in/ukLrMF8gY
One advantage of using
Deferred.await()
is that it will rethrow an exception after
CompletableDeferred.completeExceptionally
o

octylFractal

11/16/2019, 9:03 AM
that is true, though I think initializing the deferred using
async {}
would probably be best for creating the deferred if you want that behavior
e.g.
coroutineScope { async(Dispatchers.Default) { MyFactory().apply { init(); factory = this } }.apply { deferredFactory = this; await() }
err, you may want that outer
apply
inside
coroutineScope
, or it will happen too late
m

Mark

11/16/2019, 9:14 AM
What stops two threads executing that
coroutineScope
block at the same time?
o

octylFractal

11/16/2019, 9:15 AM
nothing, it's intended to go after the original isInitialized check you had above
m

Mark

11/16/2019, 9:15 AM
But two threads can get past that before the var has been initialised
o

octylFractal

11/16/2019, 9:17 AM
iirc coroutineScope doesn't (actually) suspend until the very end, so I don't think that is the case. but you can test it out
or just use the original style you had, with a try-catch for handling exceptions, but you have to be careful with CancellationException
👍 1
u

uli

11/16/2019, 9:39 AM
Why don't you just await an instance? Await can be called multiple times and will return the result immediately upon the second call
m

Mark

11/16/2019, 9:47 AM
I believe that’s what the code does
u

uli

11/16/2019, 10:02 AM
I mean make everything private and just provide one public method that awaits the deferred
I am on mobile currently so I can't provide an example
o

octylFractal

11/16/2019, 10:05 AM
I'm pretty sure that's what the code already does
u

uli

11/16/2019, 10:13 AM
Why does it need isInitialized then?
o

octylFractal

11/16/2019, 10:14 AM
to lazily initialize
u

uli

11/16/2019, 10:20 AM
Factory = async { myFactory.init() } Fun getFactory()= factory.await()
o

octylFractal

11/16/2019, 10:21 AM
that's not lazy, and it's not structured concurrency (where is the CoroutineScope for async?)
perhaps if they don't mind having a slightly wider scope, or a different lifecycle for the scope,
async(start = CoroutineStart.LAZY)
could be appropriate. But it requires restructuring regarding the scope.
u

uli

11/16/2019, 10:22 AM
I am on mobile so this was just a sketch. Add by lazy to the factory property and use a scope
m

Mark

11/16/2019, 10:59 AM
Actually I’m preferring this:
Copy code
private lateinit var deferredFactory: Deferred<MyFactory>

suspend fun getFactoryOrNull(): MyFactory? = withContext(Dispatchers.Main) {
    if (!::deferredFactory.isInitialized) {
        val deferred = CompletableDeferred<MyFactory>().apply {
            deferredFactory = this
        }
        try {
            createFactory().also {
                deferred.complete(it)
            }
        } catch (e: Exception) {
            deferred.completeExceptionally(e)
        }
    }
    try {
        deferredFactory.await()
    } catch (e: Exception) {
        loge("unable to create factory", e)
        null
    }
}

private suspend fun createFactory() = withContext(Dispatchers.Default) {
	// slow stuff
}
u

uliluckas

11/16/2019, 11:18 AM
What about this one: https://pl.kotl.in/nsfRHANl7
u

uli

11/16/2019, 11:29 AM
@Mark the isInitialized check is potentially racy. If a second call is made while getFactoryOrNull is suspended in createFactory
Just use by lazy instead of lateinit. This is a classical Singleton initialization
m

Mark

11/16/2019, 11:31 AM
But withContext(Dispatchers.Main) prevents that problem, no?
o

octylFractal

11/16/2019, 11:31 AM
yes, it's not racy due to Dispatchers.Main (no suspend points + single-threaded)
👍 1
m

Mark

11/16/2019, 11:32 AM
What I’m not convinced about is creating that CoroutineScope within
by lazy
u

uli

11/16/2019, 11:33 AM
Not if you switch context in createFactory. There you release the main thread to do other things like calling getFactoryOrNull again
What is the problem you see with by lazy?
o

octylFractal

11/16/2019, 11:33 AM
right, but the
deferredFactory
is initialized earlier than that
u

uli

11/16/2019, 11:33 AM
And the scope of not created there
m

Mark

11/16/2019, 11:33 AM
But by that point, you have already initialized the deferred
Would it matter if it was?
o

octylFractal

11/16/2019, 11:34 AM
Uli: your version would likely perform slightly better as https://pl.kotl.in/OI7YS5zWP
m

Mark

11/16/2019, 11:35 AM
Copy code
private val deferredFactory by lazy {
    CoroutineScope(SupervisorJob() + Dispatchers.Default).async { MyFactory().also { createFactory() } }
}
u

uli

11/16/2019, 11:36 AM
@octylFractal I don't think so.
@Mark by lazy is made for exactly this. It does not initialize the deferred until first access
@Mark you are right. You can create the scope inline if you prefer. And if you do not need to cancel it at any time
m

Mark

11/16/2019, 11:40 AM
I think the difference is if the coroutine calling getFactoryOrNull() is cancelled mid-call or not.
u

uli

11/16/2019, 11:40 AM
Btw. GetFactory should be static (is method on companion object)
m

Mark

11/16/2019, 11:41 AM
Needs access to instance variable
u

uli

11/16/2019, 11:41 AM
@Mark what is expected behavior then? To cancel initialization, lose all work already done and later start it again?
m

Mark

11/16/2019, 11:42 AM
You’re right. typically you wouldn’t want that
Can GlobalScope be used?
Seems to work fine: https://pl.kotl.in/FaywsrfvZ
u

uliluckas

11/16/2019, 12:05 PM
I am never sure about GlobalScope but it ooks fine to me. Maybe someone else has an opinion.
@octylFractal what do you expect CoroutineStart.LAZY to improve?
Oh, i didn't see you dropped the
by lazy
.
o

octylFractal

11/16/2019, 12:08 PM
doesn't use a lock to access the result, because yes, no
by lazy
u

uliluckas

11/16/2019, 12:08 PM
So oyurs would have a higher load at class loading but a lower at first access
I guess it still needs a lock to decide wether the async task has already been started
just a different lock 🙂
m

Mark

11/16/2019, 12:11 PM
Six of one, half a dozen of the other, though personally I prefer
by lazy
🙂 1
o
u

uli

11/16/2019, 12:19 PM
Right. It uses compareAndSet. Not sure how by lazy is initialised
o

octylFractal

11/16/2019, 12:21 PM
m

Mark

11/16/2019, 12:21 PM
But aren’t we actually comparing
by lazy
vs.
GlobalScope.async(start=LAZY)
u

uli

11/16/2019, 12:21 PM
Yes
m

Mark

11/16/2019, 12:22 PM
So nothing to do with
await
, right?
o

octylFractal

11/16/2019, 12:22 PM
I'm comparing access, not initialization
u

uli

11/16/2019, 12:22 PM
Yes
m

Mark

11/16/2019, 12:23 PM
In terms of initialization, which one is better?
o

octylFractal

11/16/2019, 12:23 PM
they're probably both about the same, they create an object that holds a lambda for later execution, with associated state
by lazy
will have a larger memory footprint because by necessity it adds another layer of object wrapping,
Lazy<Deferred<T>>
vs.
Deferred<T>
m

Mark

11/16/2019, 12:25 PM
CoroutineScope.async
seems to create quite a few new objects (well, at least two)
u

uli

11/16/2019, 2:12 PM
Do we really care as long as the later frequent access is fast?
m

Mark

11/17/2019, 5:17 AM
If it’s a rarely used factory, then I guess initialisation time is more important than access time
t

trathschlag

11/18/2019, 8:19 AM
I think this is quite similar to my question a couple of days ago: https://kotlinlang.slack.com/archives/C1CFAFJSK/p1573466026194100
This was my solution, but I haven't really checked if its working:
Copy code
interface SuspendableProvider<T> {
    suspend fun get(): T
}

fun <T : Any> suspendableLazy(provider: suspend () -> T) = object : SuspendableProvider<T> {
    val mutex = Mutex()
    lateinit var computed: T

    override suspend fun get() = mutex.withLock {
        if (!this::computed.isInitialized) {
            computed = provider()
        }

        computed
    }
}
👍 1
u

uli

11/18/2019, 8:36 AM
What about this:
Copy code
import kotlinx.coroutines.*

interface SuspendableProvider<T> {
    suspend fun get(): T
}

fun <T : Any> suspendableLazy(provider: suspend () -> T) = object : SuspendableProvider<T> {
   private val computed by lazy { GlobalScope.async { provider() } }

   override suspend fun get() = computed.await()
 
}
Or going right with my solution from above without the SuspendableProvider?
t

trathschlag

11/18/2019, 1:32 PM
This is actually what I did before, but I think cancellation is not working in this case. Assuming I cancel the
Job
suspended on
SuspendableProvider::get
, the
Job
started by
async
currently running
provider()
will not be cancelled, right? I can solve this by providing a
CoroutineScope
but I want to avoid this, as I am joining on the background coroutine immediately anyways. Logically there is no branching behaviour so I don't want that implementation detail to leak into my api.
m

Mark

11/21/2019, 4:46 AM
@trathschlag thanks for the
SuspendableProvider<T>
idea. It definitely makes it nicer. For my use case, I definitely don’t want the job to be cancellable so using
GlobalScope
makes more sense for me. Also I find using the
Mutex
with
this::computed.isInitialized
combination, a little hacky. However, I have a personal preference to use
start = CoroutineStart.LAZY
instead of
by lazy
10 Views