Explicitely defining the coroutine scope to deal with thread safety?

Suppose you have class that spawns a coroutine in its constructor to run some sort of background loop. In such a class, you of course have to pass the coroutine scope to the constructor as an argument.

Now you have some functions inside that class that may touch the same states the background loop is touching. This is fine as long as these functions run in the same thread the loop’s coroutine scope is running in. But what if the callers calls these functions from different threads?

One way would be to use coroutine mutexes. I found though that the code becomes really convoluted then. Since in my use case I do not need tons of performance, I opted for forcing all activity to run on the same thread. I came up with code like this:

class ThreadsafeEntity(private val scope: CoroutineScope) {
    init {
        scope.launch {
            println("loop started in thread with ID ${Thread.currentThread().getId()})")
            for (i in 0 until 10) {
                println("Count $i")
                delay(1000)
            }
        }
    }

    suspend fun mySuspendingFunction() {
        println("mySuspendingFunction called 1 (thread ID ${Thread.currentThread().getId()})")
        withContext(scope.coroutineContext) {
            println("mySuspendingFunction called 2 (thread ID ${Thread.currentThread().getId()})")
            // actual function logic is placed here
        }
    }

    fun myNormalFunction() {
        println("myNormalFunction called 1 (thread ID ${Thread.currentThread().getId()})")
        runBlocking(scope.coroutineContext) {
            println("myNormalFunction called 2 (thread ID ${Thread.currentThread().getId()})")
            // actual function logic is placed here
        }
    }
}

This forces all calls to run in the same thread as the background loop. For suspending functions, withContext is used. For normal functions, runBlocking is used. (These normal functions only do stuff that doesn’t block for a long or undefined amount of time, like setting a member variable’s value.)
When you run this, the “called 2” lines show that the function’s actual logic would run in the same thread as the loop, even if the function are called in separate threads (shown by the IDs).

I have two questions:

  1. Is this okay to do, or is there a smarter way to accomplish this thread safety?
  2. Is there a better way to make sure withContext and runBlocking use the context of the coroutine scope? The docs say that directly accessing coroutineContext is not recommended.
1 Like

No, this doesn’t give you any level of thread safety at all in general. A CoroutineContext contains a CoroutineDispatcher which is a thread pool, not a thread. It’s possible for this to be a thread pool of one, but nothing in your post suggests this is the case.

Also, withContext is not designed to work with a full CoroutineContext. Swapping the Job, for example, screws up Structured Concurrency since the running Job is no longed tied to the caller’s Job, but to another. Only use withContext for adjusting specific keys of the CoroutinesContext, like just the CoroutineDispatcher.

You can create a single thread dispatcher with Executors.newSingleThreadExecutor().asCoroutineDispatcher() , and wrap all your public methods and initializing code in withContext blocks that just replace the dispatcher. This will confine the code to a single thread.

If you know the CoroutineScope has a single threaded dispatcher, you can access it through scope.coroutineContext[CoroutineInterceptor] but it’s probably clearer to just have an explicit separate dispatcher.

Keep in mind coroutines can interleave even on a single thread so if you need one method to wait until the other has finished, a Dispatcher can’t help you at all. You’ll want to go with a Mutex, instead.

1 Like

Ohh I see. Is there a multiplatform equivalent to Executors.newSingleThreadExecutor().asCoroutineDispatcher() though? This is JVM specific, from what I see.

Keep in mind coroutines can interleave even on a single thread so if you need one method to wait until the other has finished, a Dispatcher can’t help you at all. You’ll want to go with a Mutex, instead.

True, but this is of no concern here. This is purely about avoiding data races.

1 Like

That’s because on K/N there isn’t any shared mutable state, so your use case just doesn’t happen there at all. on K/JS everything is single-threaded, so again it just doesn’t happen.

My 5 cents:

  1. Your code is already quite convoluted. If you use Mutex and replace all withContext(scope.coroutines) { ... } to mutex.withLock { ... } it will not become any more compliacated that it is now, yet it will become actually correct, guaranteeing that you can safely work with any mutable state that this ThreadsafeEntity contains.

  2. It is a very bad practice to initiate any kind of concurrent work from a class constructor. I highly recommend a pattern of having a separate start function as opposed to doing it from the init block.

  3. It is usually worth spending extra time working on the architecture of your application to avoid having to write such code in the first place. It is not always avoidable, but still.

  4. If all your functions have no return values (return Unit), then you can consider using an actor pattern. If not, then you can try to rearchitect it so that it becomes so (see the previous item).

2 Likes

Is there more information about this? Perhaps an article? I suppose the reason for this recommendation is that by spawning concurrent work from a constructor, nasty surprises may happen when the class is instantiated as a property for example, while an explicit start() call lets you control exactly where the concurrent work starts?

This would make sense. However, in my particular case, the coroutine runs a loop that handles incoming messages. That is, unless I send something to it, the coroutine is suspended, standing still. In that case, would it still be bad to start the coroutine in the constructor?

1 Like

See here https://www.ibm.com/developerworks/java/library/j-jtp0618/index.html

2 Likes