I’m trying to better understand a memory managemen...
# kotlin-native
k
I’m trying to better understand a memory management in Kotlin Native. I thought that accessing some property of the class from the same thread is safe, however I’m getting
InvalidMutabilityException
when launching new coroutine on Main Dispatcher (code in thread).
Copy code
class ViewModel {
    var counter: Int = 0
    val platform = Platform()

    init {
        println("init thread: ${platform.currentThread}")
    }

    fun onClick() {
        GlobalScope.launch {
            println("GlobalScope thread: ${platform.currentThread}")
            withContext(Dispatchers.Main) {
                println("withContext thread: ${platform.currentThread}")
                counter++
            }
        }
    }
}
Here is what I get printed:
Copy code
init thread: <NSThread: 0x600002164a00>{number = 1, name = main}
GlobalScope thread: <NSThread: 0x600002162b00>{number = 8, name = (null)}
withContext thread: <NSThread: 0x600002164a00>{number = 1, name = main}
so it seems like accessing
counter
is happening on the same thread as
init
has been called, but either way exception gets thrown
When I change
onClick
function to the following one:
Copy code
fun onClick() {
    println("onClick thread: ${platform.currentThread}")
    counter++
    println("counter: $counter")
}
I’m getting similar print:
Copy code
init thread: <NSThread: 0x600001738240>{number = 1, name = main}
onClick thread: <NSThread: 0x600001738240>{number = 1, name = main}
counter: 1
so I’m in the main thread, but everything is fine and crash is not happening
The question is: simple
launch
is making whole
ViewModel
class frozen and that is why I’m getting an exception?
m
Your ViewModel is captured from the closure
t
well, in any case you are referring to a field of ViewModel object class (
platform
) from GlobalScope.launch, so your ViewModel object needs to be frozen for this code to work. I’m not sure but it will probably freeze it anyway using
withContext
, also using the correct dispatcher
but to try just capture platform in a local variable first
1
m
@Tijl I don't think
withContext
freezes the block automatically?
Seems like there are valid use cases to call
withContext
without freezing its block if the dispatcher doesn't change
t
but the dispatcher does change, from whatever GlobalScope uses to main.
k
Thanks for the answers! I added printing if
ViewModel
is already frozen and you’re right:
Copy code
init thread: <NSThread: 0x600001230040>{number = 1, name = main}, isFrozen: false
GlobalScope thread: <NSThread: 0x600001228d80>{number = 8, name = (null)}, isFrozen: true
withContext thread: <NSThread: 0x600001230040>{number = 1, name = main}, isFrozen: true
t
so if you move
val platform = Platform()
to after
onClick
the first
isFrozen
should go to false, were it not for the fact that you reference it to check if it’s frozen 😃
u
Could you use
GlobalScope.launch(Dispatchers.Main)
or is this
withContext
switch integral part of your example?
I guess the following should do the trick - and also resembles bast practice from android more closely:
Copy code
fun onClick() {
        GlobalScope.launch(Dispatchers.Main) {
        withContext(Dispatchers.Default) {
             println("Dispatchers.Default thread: ${platform.currentThread}")
        }
        println("Dispatchers.Main: ${platform.currentThread}")
        counter++
    }
Still has the debug/println access to platform though
k
@Tijl when I does that:
Copy code
fun onClick() {
    GlobalScope.launch {
        val platform = Platform()
        println("GlobalScope thread: ${platform.currentThread}, isFrozen: ${platform.isFrozen(this@ViewModel)}")
        withContext(Dispatchers.Main) {
            println("withContext thread: ${platform.currentThread}, isFrozen: ${platform.isFrozen(this@ViewModel)}")
            counter++
        }
    }
}
I’m still getting:
Copy code
init thread: <NSThread: 0x6000031f0000>{number = 1, name = main}, isFrozen: false
GlobalScope thread: <NSThread: 0x6000031f2940>{number = 7, name = (null)}, isFrozen: true
withContext thread: <NSThread: 0x6000031f0000>{number = 1, name = main}, isFrozen: true
but I think this time it is because of
${platform.isFrozen(this@ViewModel)
, right?
t
still references platform from
Dispatchers.Default
, so the whole viewmodel will be frozen
@KamilH you’re doing exactly as I predicted, you’re referencing viewmodel to check if it’s frozen, which causes it to get frozen:
${platform.isFrozen(this@ViewModel)}
Copy code
fun onClick() {
    val platform = Platform()
    GlobalScope.launch {
        println("GlobalScope thread:             ${platform.currentThread}")}.join()
   println("isFrozen: ${platform.isFrozen(this@ViewModel)}"
// ..
no garantuees, but I think you will get false now
u
I guess
platform.currentThread
is enough to get
platform
frozen
t
anything is enough
it needs to be frozen to access it, at all, from another thread
u
Get rid of the debug code and it should work:
Copy code
fun onClick() {
        GlobalScope.launch(Dispatchers.Main) {
        withContext(Dispatchers.Default) {
             println("Doing something in the background, not accessing the view model")
        }
        println("Dispatchers.Main thread: ${platform.currentThread}, isFrozen: ${platform.isFrozen(this@ViewModel)}")
        counter++
    }
}
t
but it doesn’t get frozen magically, it’s launch that freezes the code block which references
platform
(and if it’s a field in viewmodel it references viewmodel)
u
yes. launch freezes it’s lambda (hopefully only on thread change) which references
this
to reference
platform
to reference
currentThread
which freezes the whole chain, including
this
(aka instance of ViewModel)
Sorry, just noticed, that platform is a local now in your example.
It is actually a common pattern to launch everything view-related in a scope on the main thread and only do withContext when you come to blocking code
t
I think yours should work now @uli , I checked the actual code,
Copy code
val curThread = currentThread()
    val newThread = completion.context[ContinuationInterceptor].thread()
if (newThread != curThread) {
        check(start != CoroutineStart.UNDISPATCHED) {
            "Cannot start an undispatched coroutine in another thread $newThread from current $curThread"
        }
        block.freeze()
so indeed when coming from a thread (even if it has no dispatcher) into a dispatcher with the same thread no freeze takes place
👍 2
this was a gap in my knowledge (didn’t know if it was dispatcher thread based or just any thread), so interesting conversation
k
Thank you, very interesting conversation, indeed. Just one last question to be sure. When I does that:
Copy code
fun onClick() {
    GlobalScope.launch(Dispatchers.Main) {
        counter++
        println(counter)
    }
}
I’m not getting exception, while with `withContext`:
Copy code
fun onClick() {
    GlobalScope.launch(Dispatchers.Default) {
        withContext(Dispatchers.Main) {
            counter++
            println(counter)
        }
    }
}
I’m getting an exception and that is because the thread switching happens here (Default -> Main) and in that time
ViewModel
freeze is happening, right?
t
the code block
Copy code
{
            counter++
            println(counter)
        }
this block itself is created in globalscope, and has to run in main. so the block itself (and everything it references) gets frozen
u
No. The freezing already happens when you launch the coroutine on the Default dispatcher. It does not happen, when you access the counter (in viewmodel) but when you pass it to another thread
t
you can see it in the code from the internals I posted, last line:
block.freeze()
u
So your block with the
this
reference get passed to a new thread by
launch
just to get passed back by
withContext
. This is what causes your freezing. And that’s what is solved by my suggestion
k
Thank you, that makes sense now. It would be easier to understand if we could attach callback to
Any
to see when it’s getting frozen 🙂
u
The coroutines way to do it is to create a CoroutineScope with Dispatcher.Main in your ViewModel and launch all coroutines on that scope. You can also cancel the scope then in viewWillDisappear
k
Yes, sure this is what I do, but I wanted to better understand the
freezing
stuff 🙂
u
Copy code
val vmScope = CoroutineScope(Job() + Dispatchers.Main)

fun onClick() {
        vmScope.launch {
        withContext(Dispatchers.Default) {
             println("Doing something in the background, not accessing the view model")
        }
        println("vmScope thread: ${platform.currentThread}, isFrozen: ${platform.isFrozen(this@ViewModel)}")
        counter++
    }
}
t
essentially the inverse is usually better:
Copy code
fun onClick() {
   MainScope().launch {
       // we came from main thread, nothing is frozen
       withContext(Dispatchers.Default) {
           // do background work but make sure not to needlessly capture stuff from the viewmodel
       }
   }
}
👍 1
keep in mind that even when you do android stuff, capturing variables from your viewmodel in another thread is also bad practise even though it doesn’t crash. you’re likely to get concurrency issues, you’ll hold on to references to the viewmodel in the background when the viewmodel is supposed to be GC’ed long ago, etc
👍 2
u
unless you cancel your scope 😉
t
also for that reason don’t use
MainScope()
, use a scope tied to the lifecycle of your viewmodel with the Dispatchers.Main(.immidiate)
👍 1
likewise never use GlobelScope unless you really know what you are doing
👍 1
as @uli posted. more or less 🙂
k
So you says that doing something like that:
Copy code
class ViewModel {
    private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
    val state = MutableStateFlow("")
    fun loadData() {
        scope.launch { 
            state.value = longRunningJob()
        }
    }
    
    private suspend fun longRunningJob(): String =
        withContext(Dispatchers.Default) {
            Thread.sleep(5000)
            ""
        }
}
in Android world is a bad practise because I’m capturing
ViewModel's
variable (
state
)? Or because of the fact that
scope
is running in
Main
and only
longRunningJob
makes Dispatcher switching it’s safe?
🚫 1
I’m putting long running jobs into `Interactor`/`Repository` layer. Those are doing dispatcher switching to
Default
and I’m calling them from
Main
. I always thought it’s safe 😅
u
What you posted I’d consider good practice because: • You do not capture the view model unnecessarily within the default dispatcher • You do not switch threads except where explictily needed And as you mentioned, the actual long running jobs probably come from `Interactor`/`Repository` layer where the switching would be done. Side note: You could update your example to use Thread.sleep(5000) to emphasise that it is blocking. If it would just
delay
you could stay on the main thread
k
Updated 🙂 Thank you, that makes a lot sense