Multi-threading in a Kotlin MPP project is quite d...
# kotlin-native
s
Multi-threading in a Kotlin MPP project is quite difficult due to the limitations that the K/N Worker API sets in place 😞
a
What limitations of the Worker API?
s
the job that is required to be detached from everything but the single producer
due to the
mutable XOR global
thread-safety requirement
If I want to perform background work on K/N I need to use the worker API which requires structuring things in such a way that the object passed to execute is detached. Versus say using coroutines to run on JVM which has no such restrictions
It’s hard to say generically in an MPP “do X work in the background” when doing so for mac/iOS requires special rules and two lambdas vs JVM which does not have the same restrictions and typically only a single lambda is needed
a
Yeah, for me it also was a surprise, but now I actually like the restrictions. You can use immutable data structures and atomics to "update" shared state. Or use Stately collections lib. No need to detach anything.
s
I agree, it’s not an issue of working with the K/N multi-threading restrictions, it’s integrating it in an MPP that has a cohesive experience for executing async code in the background
I can’t think of any good way to create a good API using expect/actual classes that utilize Worker for mac/iOS but coroutines for android
a
You can abstract background work in common, and use Worker API in K/N and a thread pool in JVM
Or coroutines in JVM if you prefer
s
I know I could but it is a headache. If I want to say
val job: () -> Unit = { ... }
, in JVM I can reference objects outside of the lambda freely, in K/N I cannot
a
You can, but they need to be frozen.
s
well, yes
a
You can also define expect/actual freeze function
s
I have done that already
a
For me it is totally fine
s
But even if objects are frozen in K/N you cannot reference them directly from the job, you can only if they are part of the object coming from the producer
And JVM has no concept of a producer
k
Freeze the job
?
a
Also you do some job in background, then pass the result to the main thread and then there's no need to freeze outside world
You can reference, just pass a frozen lambda into worker and call it from background
k
That’s what we’re doing. Keep whatever callback you want on the main thread local to main thread, the background gets frozen along with it’s data, and when you get back to the main thread, you have the unfrozen callback to run.
s
Copy code
val a = 1
a.freeze()

// succeeds
worker.execute(TransferMode.SAFE, { a }) {
     print(it) 
}

// fails
worker.execute(TransferMode.SAFE, { a }) {
    print(a)
}
k
This will be easier when MT coroutines happen and/or with libraries, but unless they change the memory model, you’ll still need to understand how freezing etc works. It’s definitely confusing the first time you look at it, though.
s
I understand freezing, it just makes MPP harder to implement
a
Instead of passing an object, pass a frozen lambda. Then call it from inside the Worker.
s
In my use case the lambda cannot be frozen
a
It should be fine to freeze a lambda, just make sure you capture only needed objects
k
Yeah, that. I was trying to remember the syntax we were using. I haven’t had to do it in a while (internal libraries for concurrency)
Copy code
val workLambda = {
            print(a)
        }.freeze()
        
        worker.execute(TransferMode.SAFE, { workLambda }){
            it()
        }
s
yeah that is a good way to do it. Just sad that it makes the same restriction for K/N necessary for other targets in MPP
thanks for the suggestion though, makes the approach more straightforward
@kpgalligan do you remember of a way you cleaned up workers? I have
actual fun performAsync(block: () -> Unit)
. Unless I put it in a class that stores the workers and has an API to manage, I don’t know of a way to request termination of the worker on job completion since you can’t reference the worker in the job AFAIK
Actually I think I can just do
Copy code
val worker = Worker.start()
worker.execute(...)
worker.requestTermination(processScheduledJobs = true)
default is true anyways though, looks like it will terminate after the scheduled job completes
k
You shouldn’t kill the worker if you’re going to be sending more jobs. I believe that creates/kills thread each time. We have a couple levels of indirection because we also wanted to test logic, in which case we don’t go to a background
s
yeah makes sense. I will likely just create a pool of workers that get cycled between
In that example, I’m basically using 2. 1 for local operations to the device, and one for network. I think we moved to ktor, so that network one isn’t super useful anymore (the app needs some cleanup)