This tripped me the other day when I was trying to figure out why objects were getting frozen. When Ben suggested a launch or withContext lambda parameter was itself frozen, it took me by surprise because I really don't think of the trailing lambda parameters as object references when I see them. If I write with the 2nd syntax above it's clear to me, even without extracting the lambda as a local variable, the named parameter really helps.
I am almost wrapped on my first multiplatform feature which uses coredux to implement a state machine in pure kotlin that emits immutable state objects to a ViewModel. Coredux is a library that implements the redux pattern with channels and coroutines. The observer of the ViewModel in my ViewController is getting frozen. Actually I pushed a freeze problem that started in my reducer down to the ViewController by copying a freeze + atomicRef wrap pattern from the DroidconKotlin repository 🤦♂️
That started to feel wrong. I am fairly confident that I am not sharing mutable state (open-minded to perhaps there is some subtle way I am!). I shouldn't have to incur the cost of atomic operations for operations that are just happening on the UI thread and I believe my code guarantees that those operations will happen in the order I expect.
I think the problem is how do you avoid freezing any listener/callback on the main thread. Whatever work you do on a background thread must have a reference to the object that started that work, so it can pass back results. That reference on the original thread will have to get frozen, and then thanks to recursive freezing, everything all the way back to your observer on the UI thread gets frozen.
I probably got tripped up as a result of not thoroughly thinking through how suspend functions and channels must be implemented themselves. I started using coredux with the mindset of
ah! channels are for concurrency, they probably do something magical for me to ensure I can move objects between threads and guarantee a happens-before relationship, maybe through some IPC-like serialize-deserialize mechanism, and then kotlin will be polite and not freeze objects emitted on a ReceiveChannel. I vaguely remember a Dan Lew talk showing a bunch of nested Continuation objects and those being what suspend functions are compiled to. I don't know how channels are implemented but if I try to imagine, I cannot imagine how there would not be a chain of references from a background thread to the main thread if the background thread communicates with the main thread.
My motivation for going down this rabbit hole is I really want to get manual transpilation out of my life 🙂 So I have been doing the minimal deep diving I can get away with to move forward on feature development. Sometimes that has meant trying to figure out this concurrency stuff in the feature development cycle. The multiplatform ios development cycle is too long for that. I still have some lingering questions on how channels and suspend functions work. I am gonna switch tack to writing ios unit tests and using the checkIsFrozen functions to get a better grasp on how all this works.
I need to transfer knowledge from a background thread to the UI thread without triggering this recursive freeze. My hunch is that I have two options left: use
https://github.com/Autodesk/coroutineworker or figure out how to make that the ui thread reference from the background thread is an atomicreference to prevent recursive freezing. My guess is that coroutineworker does something like the latter.
Anyways, just sharing in the hopes that this might help other folks who are slogging through similar issues and give insight to the kotlin team to how a kmp newby is approaching these libraries.