Home > Software design >  Explanation of intriguing behavior of coroutines
Explanation of intriguing behavior of coroutines

Time:02-23

I have a method that runs some operations in a row. Is is actually a for loop, which loops it's contents 50 times, each iteration taking roughly 0.2 seconds. I have a constant animation being presented on the screen for the duration of this execution. So, it is obvious that I wish to carry these operations off the main thread, so my animation can keep up (or the recompositions can take place, this is Compose). What I realized is, that this simple method

fun run(){
        repeat(10000) {
            repeat(5000){
                print("I ♥ Kotlin")
            }
        }
    }

if run in a standard Composable scope just like that, will block the UI thread as one would expect.

b) it would also block the UI thread if I call it in a LaunchedEffect while nesting it in a call to launch{...}.

c) It does not block if I run it on an I/O coroutine, which is also the default coroutine.

d) the app sometimes crashes if run on the Main Dispatcher

Now, simple question - why is this?

LaunchedEffect(Unit){
 run() // Block
}
Launchedeffect(Unit){
 launch{
  run() // Block
 }
}
LaunchedEffect(Unit){
 withContext(Dispatchers.Main){
  run() //Blocks, and at times, crashes
 }
}
LaunchedEffect(Unit){
 withContext(Dispatchers.IO){
  run() // Runs without blocking
 }
}
thread{
 run() //Runs without blocking, no crash
}

Can anyone explain why the Dispatchers.IO works and the others don't? It's sort of giving me undesired stress.

If anyone requires a quick animation UI to test it out, here it is

@Composable
fun DUM_E_MARK_II() {

    val sizeTransition = rememberInfiniteTransition()

    val size by sizeTransition.animateFloat(
        initialValue = 50f,
        targetValue = 200f,
        animationSpec = infiniteRepeatable(
            keyframes { durationMillis = 1000 },
            repeatMode = RepeatMode.Reverse,
        )
    )

    Icon(
        imageVector = Icons.Filled.Warning,
        contentDescription = "",
        modifier = Modifier.size(size.dp),
        tint = Color.Red
    )

}

CodePudding user response:

Your code is a long-running, non-suspendable task. It blocks whatever thread it runs on for its entire lifetime. When you block the UI thread, it causes the UI to freeze, and after a timeout Android kills such a misbehaving application.

If you use any dispatcher that uses its own thread pool, for example IO, the task will block a non-UI thread.

CodePudding user response:

 withContext(Dispatchers.IO){
  run() // Runs without blocking
 }

here, you're explicitly saying that you want to run this on another thread, specifically a thread which won't have an impact on the main thread, so when you call:

withContext(Dispatchers.Main){
  run() //Blocks, and at times, crashes
 }

then yes, this probably should crash with ANR exception, because the main thread has been blocked for too long, that's the point of withContext is to specify where this work should be done, and intensive or long running tasks should not be on Dispatchers.Main

This function uses dispatcher from the new context, shifting execution of the block into the different thread if a new dispatcher is specified, and back to the original dispatcher when it completes.

CodePudding user response:

run() function is a long-running function, it will block the thread which executes it.

Let's consider each case one by one:

  1. run() function is invoked in Main(UI) thread, blocking it.

    LaunchedEffect(Unit) {
        run() // Block
    }
    
  2. run() is invoked inside a coroutine, which is launched using launch coroutine builder. The context of the coroutine is the composition's CoroutineContext, I assume it consists of Dispatchers.Main dispatcher. So the run function is also invoked in the Main(UI) thread, blocking it.

    Launchedeffect(Unit) {
      launch {
       run() // Block
      }
    }
    

You can make the run() function suspend using withContext(Dispatchers.IO), it will switch the execution context of the run function to Dispatchers.IO thread pool:

   suspend fun run() = withContext(Dispatchers.IO) {
       // this is executed in background thread
   }

   Launchedeffect(Unit) {
     run() // Not Blocking
   }

   Launchedeffect(Unit) {
     launch {
       run() // Not Blocking
     }
   }
  1. run() function is invoked in Main(UI) thread, blocking it, because Dispatchers.Main is used for its context execution. Dispatchers.Main executes a coroutine in the Main(UI) thread.

    LaunchedEffect(Unit){
      withContext(Dispatchers.Main){
        run() // Blocks, and at times, crashes
      }
    }
    
  2. In this case it runs without blocking because Dispatchers.IO is used as a coroutine context. It uses background pool of threads. It will not block the Main thread because it executes in background thread.

    LaunchedEffect(Unit){
      withContext(Dispatchers.IO){
       run() // Runs without blocking
      }
    }
    
  3. This runs without blocking the Main thread because another thread (background thread) is used to execute it.

    thread{
       run() //Runs without blocking, no crash
    }
    
  • Related