I have trouble understanding the Kotlin memory mod...
# multiplatform
s
I have trouble understanding the Kotlin memory model. I get
Uncaught Kotlin exception: kotlin.Throwable: The process was terminated due to the unhandled exception thrown in the coroutine [StandaloneCoroutine{Cancelling}@2929dd8, MainDispatcher]: mutation attempt of frozen kotlin.collections.HashMap@3b0368
if I do this:
Copy code
val newPhotoSourceSyncInfo = mutableMapOf<PhotoSource, SyncInfo>()
newPhotoSourceSyncInfo.putAll(oldState.photoSourceSyncInfo)
newPhotoSourceSyncInfo[photoSource] = syncInfo

return oldState.copy(
    photoSourceSyncInfo = newPhotoSourceSyncInfo
)
But I can do this without any problem:
Copy code
val newPhotoSources = mutableListOf<PhotoSource>()
newPhotoSources.addAll(oldState.photoSources)

newPhotoSources.add(
    PhotoSource(
        id = 1
    )
)

return oldState.copy(
    photoSources = newPhotoSources,
)
Why is that for a HashMap different and how can I do it to please the memory model?
1
m
I think the freeze rules do not apply to classes from Objective-C. I'm guessing that
mutableMapOf
delegates to
NSDictionary
, but
HashMap
is using a pure Kotlin class.
🙏 1
s
Do you mean
mutableListOf
?
m
Id did. So I don't know the type of object returned by
mutableMapOf
, from the first example, but would have expected it to also be a native class that ignores freezing. In general even if sample two works, I would avoid it and strictly follow the kotlin native memory model. Assume any object accessible by multiple threads are frozen and do not modify them.
👍 1
s
Ok, that sounds reasonable. How would you change my code? Some call that creates a new list plus that entry or what is the correct approach?
m
The snips are too small for me to see what is happening in different threads, what properties are accessible by multiple threads or what you are trying to do.
s
I just look up if there a one-shot commands in Kotlin that creates a new list/map with one added/removed entry without modifying one list
A, there is
plus()
. I try that
Ok, that did not help. Behind the scenes it does the same. I hoped somehow it has a native implementation that creates a new Collection without modifying a temporary one...
m
Without studying the whole app, I would assume that
addFeed
is called on the main thread. Ideally what you would want to do in your example, is get the new data from a background thread if needed and then update the map on the main thread. Try to leave mutable state on just the main thread. If you do need to modify maps on multiple threads, you could look at the stately library.
👍 1
🙏 1
s
Ok, thank you for your advice. I will see how I can change my code so that there is a better isolation.
k
I think the freeze rules do not apply to classes from Objective-C.  I'm guessing that 
mutableMapOf
 delegates to 
NSDictionary
, but 
HashMap
 is using a pure Kotlin class
mutableMapOf does not use NSDictionary
I don't think so. NSDictionary is a map.
mutableMapOf
does not use that, though. It would be easier to figure this out with an example to run, and more detail from the stack trace. Exactly where is it getting upset?
s
Ok, understand.
k
My guess is when copying from the original map, the internal try to optimize by reusing map entries or something like that, and that's where it fails.
In fact, if it were using NSDictionary, you'd be avoiding this particular issue (but you'd be in danger of others).
I do that to avoid freeze rules in a sqlite driver, but you need to be very careful with it.
Anyway, this looks like it should work. If it's failing, I'd be interested in poking around a bit more (for context, I've discussed this a lot:

https://www.youtube.com/watch?v=oxQ6e1VeH4M

)
s
I have a coroutine running on
Dispatchers.Default
calling my method. This method launches a new Coroutine on
Dispatchers.Main
to manipulate this map and that's where it fails. Changing the external Coroutine to
Main
let's it work. And yes, that may not be a List vs Map difference because I double-checked that indeed all List handling methods actually get called from Main coroutines. So that was my wrong assumption.
That's my current state of the method...
Copy code
private fun onSyncStatusUpdate(
    oldState: PhotoState,
    photoSource: PhotoSource,
    syncInfo: SyncInfo
): PhotoState {

    print("onSyncStatusUpdate() method entry: ")
    logCurrentThread()

    launch(Dispatchers.Main) {

        print("onSyncStatusUpdate() launch block: ")
        logCurrentThread()

        val newPhotoSourceSyncInfo =
            oldState.photoSourceSyncInfo + (photoSource to syncInfo)

        // FIXME Use store.dispatch()
        state.value = oldState.copy(
            photoSourceSyncInfo = newPhotoSourceSyncInfo
        )
    }

    return oldState
}
So I'm not sure why this still fails because now the map modifying operation is on Main Thread... 🤷‍♂️
I will watch the linked youtube video and hope to understand all this better
k
The video is a deep dive. I wrote this after the video as more of an overview: https://dev.to/touchlab/practical-kotlin-native-concurrency-ac7
s
Ok, thank you. I will read it and try to see where my thinking in wrong.
k
If there's a way to get a failing repro that I can run somewhere, it would be a lot easier to figure out. In the meantime, I'd probably start with building the new map "the hard way", as in create a new mutable map and loop through each key/value of the old one, rather than "putAll" or
oldState.photoSourceSyncInfo + (photoSource to syncInfo)
. If the code is failing on that line, I'd assume the map is trying to optimize something under the hood with a side effect that is incompatible with the freeze model. I'd also confirm it's exactly that and not somewhere else.
I find the freeze model not too bad once you get used to it, but I have a very skewed perspective at this point. Also, some things are obvious, but some things are very difficult to spot, and occasionally they're extra weird.
s
Can a whole
Map
get frozen if I put a
key
into it that is frozen?
In your talk you said that it freezes "everything".
k
Well, when you run
freeze()
, that will recursively freeze everything that the object is touching. If a key is already frozen, adding that to a map won't freeze the map.
🙏 1
s
@kpgalligan Do you know why a regular lambda freeze also freezes the implicit "this" and a Flow.collect() lambda does not? I think I don't quite understand the inner workings of that yet, so I'm a bit surprised about that. https://kotlinlang.slack.com/archives/C3PQML5NU/p1636379868397700
k
I've been getting multiple questions like this over the past 24 hours. Feels like I'm in an interview 🙂 I'll take a look...
😄 1
Long thread. If you have an update of the code as is, or something to run, it would be much easier to debug. Well, maybe not "much", but these issues can be difficult to reason in the abstract. I would put
ensureNeverFrozen()
on anything that shouldn't be frozen (and maybe leave it there). We do it proactively on certain types of objects (view models, etc)
s
We do it proactively on certain types of objects (view models, etc)
Yes, very good idea. After fixing multiple problems with accidently frozen state I now spam
ensureNeverFrozen()
all over the place for fail fast.