Coroutine dispatcher threads are orphaned in OSGI environment

My application runs as a plugin to another app which can get reinstalled multiple times, each time reloading all its classes, including kotlin ones (it uses OSGI). Right now, coroutine threads from thread pool are orphaned after reinstallation, causing memory leak.

Is there any way to shutdown underlying threads from coroutine dispatchers? (I’m using default one and IO)

One solution I see, is to provide my custom thread pool, but wouldn’t that lack some optimizations, like no need for thread switching when switching between default and IO dispatchers?

No. My understanding is that the optimization you are talking about doesn’t exist currently though it may in the future. Switching from Default to IO will always switch threads, currently.

You may be thinking of switching between coroutines within the same dispatcher in which case a thread switch does not need to occur. This should work with custom thread pools too.

I was basing on that doc in Dispatchers.IO:

This dispatcher shares threads with a Default dispatcher, so using withContext(Dispatchers.IO) { … } does not lead to an actual switching to another thread — typically execution continues in the same thread.

I understand these two dispatchers share same thread pool, but I’m not sure I’d achieve the same using my custom thread pools(separate for blocking IO and CPU), since there are probably some other optimizations, related to these two, like using ExperimentalCoroutineDispatcher which I can’t get direct access to.

Sorry, totally wrong on that one.

You can likely gain the same benefit with a helper method. This uses the back-pressure implicit in a limited capacity channel to constrain the number of coroutines in flight at the same time like Dispatchers.Default (not tested, just an idea):

val defaultChannel = Channel<Unit>(determineDefaultCount())
suspend fun <R> withDefault(block: suspend CoroutineScope.() -> R): R {
    defaultChannel.send(Unit) //Takes up slot in buffer, blocks if buffer is full
    return try {
        withContext(customPool, block)
    } finally {
        defaultChannel.receive() //Gives slot back

You can also take inspiration from Dispatchers.Default itself: Keep a single dispatcher that has your custom thread pool and then create a wrapper dispatcher, that queues up tasks if more than N are running. After a task finishes check the queue and dispatch up to N if needed.

You probably want to recreate the original dispatchers in a way that you can shut down the corresponding thread pools on OSGi unload (after you have cancelled all coroutines). There is some chance though that it is possible to do this with the default dispatchers - have a dig around (it may also be a valid feature request)