How can I use co-routines to single-thread asynchronous responses

Yes, this implementation seems fine and it should meet all your requirements. There is a race condition on add() function: if multiple threads invoke this function concurrently, you don’t have guarantees which task will be added first. It may violate your first requirement, but I guess this shouldn’t be a problem. You can always synchronize this function if you need.

Note that Channel.BUFFERED does not create a channel with unlimited capacity. It has a fixed size and after filling it up, your add() will block, waiting for free space. I’m not sure if this is what you expect. If not then use Channel.UNLIMITED instead and then I think it would be better to implement add() as:

channel.trySend(block).getOrThrow()

runBlocking() always has some performance hit and we don’t really need to suspend if the channel is unlimited. Maybe I miss something here.

Also, someone will probably tell you that GlobalScope is evil, but I believe you used it correctly. Your solution is functionally the same as creating the scope with CoroutineScope(). You can simplify your loop to for (item in channel) or channel.consumeEach {}, but this is just a small improvement.

Also, keep in mind that coroutines are always a little cumbersome when you need to use them from outside of coroutines context and especially for some little feature. This is due to this scope and structured concurrency things. For this reason it may not be always a good idea to migrate to coroutines just to simplify the code. However, if you go fully-coroutines, many things become simpler. For example, your example could be reimplemented as:

class DispatchQueue {
    val channel = ...
    fun addTask() { ... }
    suspend fun run() {
        channel.consumeEach { ... }
    }
}

class AnotherService {
    suspend fun run() = coroutineScope {
        launch { DispatchQueue().run() }
        
        ...
    }
}

As coroutines support cancelling automatically, we don’t need to bother about creating the scope and cancelling manually. If we cancel coroutine that started AnotherService, it will automatically cancel all services, including the loop in DispatchQueue.

1 Like