Coroutines: Correct use for class managing data

I am trying to understand what would be the best practice (or even correct way) to make a class that manages data use coroutine async-ness.

I’ve come up with this:

class MyManager : CoroutineScope by CoroutineScope(Dispatchers.Default) {

  private var myState: Stuff

  /* Slow getter-function without side-effects.

     This should be cancellable by the caller. F.e. if it was called
     by the UI and the UI is being destroyed.
  */
  suspend fun getThings(): List<Things> {
    // need to fetch data from file, so switching context....
    return withContext(Dispatchers.IO) {
        // fetch some Things from file
    }
  }


  /* Mutating function without result (but sideeffects). 

     This should NOT be cancellable by the caller. F.e. if it was called
     by the UI and the UI is being destroyed, exec should continue because 
     myState must be in sync with whatever is being persisted */

  @Synchronized fun addThings(things: Iterable<Thing>) {
    launch {
      // need to fetch data from file, so switching context....
      withContext(Dispatchers.IO) {
        // add some Things to file
      }
      myState = doSomeLengthyCalculation()
    }
  }

  /* Mutating function with result (but sideeffects). 

     This should NOT be cancellable by the caller. F.e. if it was called
     by the UI and the UI is being destroyed, exec should continue because 
     myState must be in sync with whatever is being persisted */
  @Synchronized suspend fun addThingsAndReturnResult(things: Iterable<Things>): Int {
    return async {
      withContext(Dispatchers.IO) {
        // add some Things to file
      }
      myState = doSomeLengthyCalculation()
      myState
    }.await()
}

This topic is related: Best practices for Kotlin coroutine/async library functions
I’ve read that, and the linked article, and from these, I understand as much as that the above example won’t work, but I fail to understand how else this would need to look then.

Can anyone help me wrap my head around this?

In particular:

  1. Will getThings really be cancelled if the scope of the caller is cancelled? What is the parent scope of that withContext anyway, the caller or MyManager?
  2. Will addThingsAndReturnResult really not get cancelled if the scope of the caller is cancelled? What is the parent scope of that async anyway, MyManager or the caller?
  3. That async {}.await() looks wrong. But how else to do this then?

Avoid implementing CoroutineScope, just keep it as a private member property when you need one. It’s not really part of your MyManager API. Also, it makes harder to identify where it’s used if the usages are all with the implicit this (myScope.launch vs just launch)

@Synchronized does not apply to callbacks within a method so your lambdas passed to async/launch/withContext are not getting synchronized. And Kotlin transforms suspend methods such that the code ends up in callbacks. You’ll want to use Mutex instead.

Yes

withContext is not a CoroutineScope extension method nor is CoroutineScope a parameter. It has no access to MyManager. Check the signature to see if a method uses a CoroutineScope. Keeping CoroutineScope as a property will make it clearer (you’ll see myScope.launch and withContext(Dispatchers.IO) )

addThingsAndReturnResult(specifically await) will get cancelled but the code in the lambda will not.

It’s MyManager. That async is an extension method on CoroutineScope that does not suspend.

withContext(NonCancellable + Dispatchers.IO) is probably what you want. It’s the Job of the CoroutinesContext that gets cancelled. NonCancellable is a Job that ignores cancel calls.

Honestly, it doesn’t seem like you need CoroutineScope in this class. If all you methods suspend (consistency is good anyways), I think withContext will address your needs.

In general, methods related to CoroutineScope do not suspend (they kick off coroutines and immediately return) while methods that suspend are only affected by the current coroutine’s CoroutineContext which always comes from the caller.

2 Likes

Wow, thank you for all the hints!

Avoid implementing CoroutineScope […]

Sounds reasonable. Is there any situation where implementing CoroutineScope in a class makes sense in your opinion? Currently, basically everywhere I use coroutines, I use this pattern, because I thought it was the recommended way to do.
But I (also) find the this-confusion in Kotlin very inconvenient.

withContext(NonCancellable + Dispatchers.IO) is probably what you want. It’s the Job of the CoroutinesContext that gets cancelled. NonCancellable is a Job that ignores cancel calls.
Honestly, it doesn’t seem like you need CoroutineScope in this class. If all you methods suspend (consistency is good anyways), I think withContext will address your needs.

That’s interesting… hmm. I have been looking at this from an object oriented perspective: That MyManager should be self-contained in the sense that it manages that data, synchronizes access to it etc. and also is the master of any threads / coroutines it employs. In a classical Java-thread(-pool) approach, that class would probably own some worker thread that does the doSomeLengthyCalculation.
Not granting that class ownership over its coroutine scope and creating the NonCancellable Job feels like it is encroaching on a scope it doesn’t own.

I conclude that I must catch up on the topic of how CoroutineContext inperlay with CoroutineScope and Jobs.

@Synchronized

Thank you for the tip on Mutex. To paraphrase, synchronized doesn’t work as intended because the lock held is only valid for the method body, which is in this case just the call to the launch/async method which immediately returns, right?

If you are creating something for launching coroutines with some extra behavior (like ProducerScope)

Control synchronization with Mutex. Control threads, jobs, and anything else through withContext. You don’t lose any control by using a suspend method, you just end up with an async API that reads more like a regular sequential blocking API.

I recommend reading Coroutine Context and Scope

Correct.

1 Like

I read through the Coroutine Context and Scope link, and I must say that I found it really hard to understand. However, the links to further articles by the author at the bottom of that article that explained more of the basics were helpful to to me to understand that article.

Having concerned myself with this topic and best practices in general in the last days, I also can recommend the Coroutines Best Practices article on developer.android.com. I don’t use much of the Android architecture components for my project, but still the best practices mentioned in the article make sense generally and helped me to understand better how coroutines should be used when integrated into a larger application architecture.