Dispatchers.Default

Dispatchers.Default

  • CPU-intensive work (calculations, data processing)
  • Thread pool size equals CPU cores

Questions that come up

Should we limit max concurrency by one less thread for heavy workloads to leave some for UI?

Should we limit the concurrency of Dispatchers.Default by creating a limitedParallelism dispatcher?

// Like this:
val maxConcurrency = (Runtime.getRuntime().availableProcessors() - 1).coerceAtLeast(1)
val limitedDispatcher = dispatcher.limitedParallelism(maxConcurrency)

Why would we want to limitedParallelism? Is due to the following question: if you use up all threads will operating system (OS) create cooperative environment for other processes that are running?.

Experiment aimed to roughly answer this

Experiment: load up CPU and try to use other apps in the meantime.

When I tried this on 16-core/32-thread AMD Ryzen 9 7945HX on Linux it seems at least Linux OS was creating a cooperative environment between the threads to not show perceivable slow down in other processes while the CPU was shown to be running at 100% per htop. Hence, as of now I don't think it's worth artificially lowering the parallelism and rather let OS take care of scheduling threads.

img

GT-Sandbox-Snapshot: example loading up all the CPU cores for 30 seconds doing computation in a loop

Code

package com.glassthought.sandbox

import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

fun main(): Unit = runBlocking(CoroutineName("main-runBlocking")) {
  val availableProcessors = Runtime.getRuntime().availableProcessors()

  println("Starting CPU stress test on $availableProcessors cores...")
  println("This will run for 30 seconds. System may become less responsive.")

  val duration = 30_000L // 30 seconds in milliseconds

  val elapsedTime = measureTimeMillis {
    // Create a list of jobs, one for each processor
    val jobs = List(availableProcessors) { coreIndex ->
      // Use Dispatchers.Default which has a thread pool sized to CPU cores
      // Each coroutine will pin to a thread and consume CPU
      launch(Dispatchers.Default + CoroutineName("cpu-loader-$coreIndex")) {
        println("Core $coreIndex: Starting intensive computation...")

        val startTime = System.currentTimeMillis()
        var counter = 0L
        var sum = 0.0

        // Tight loop that runs for 30 seconds
        while (System.currentTimeMillis() - startTime < duration) {
          // Perform some CPU-intensive operations
          counter++
          sum += Math.sqrt(counter.toDouble())

          // Add some more operations to ensure CPU usage
          if (counter % 1000000 == 0L) {
            // Occasionally perform more complex calculations
            for (i in 1..100) {
              sum += Math.sin(sum) * Math.cos(counter.toDouble())
            }
          }

          // Check for cancellation periodically (every million iterations)
          if (counter % 10000000 == 0L) {
            yield() // Allow coroutine cancellation if needed
          }
        }

        println("Core $coreIndex: Completed. Counter=$counter, Sum=$sum")
      }
    }

    // Wait for all jobs to complete
    jobs.joinAll()
  }

  println("\nCPU stress test completed in ${elapsedTime}ms")
  println("All $availableProcessors cores were loaded for approximately 30 seconds")
}

Command to reproduce:

gt.sandbox.checkout.commit 552c189a3d508ae510c9 \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./gradlew run --quiet"

Recorded output of command:

Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
Starting CPU stress test on 32 cores...
This will run for 30 seconds. System may become less responsive.
Core 0: Starting intensive computation...
Core 1: Starting intensive computation...
Core 2: Starting intensive computation...
Core 3: Starting intensive computation...
Core 4: Starting intensive computation...
Core 5: Starting intensive computation...
Core 6: Starting intensive computation...
Core 7: Starting intensive computation...
Core 8: Starting intensive computation...
Core 9: Starting intensive computation...
Core 10: Starting intensive computation...
Core 11: Starting intensive computation...
Core 12: Starting intensive computation...
Core 13: Starting intensive computation...
Core 14: Starting intensive computation...
Core 15: Starting intensive computation...
Core 16: Starting intensive computation...
Core 17: Starting intensive computation...
Core 18: Starting intensive computation...
Core 19: Starting intensive computation...
Core 20: Starting intensive computation...
Core 21: Starting intensive computation...
Core 22: Starting intensive computation...
Core 23: Starting intensive computation...
Core 24: Starting intensive computation...
Core 25: Starting intensive computation...
Core 26: Starting intensive computation...
Core 27: Starting intensive computation...
Core 29: Starting intensive computation...
Core 28: Starting intensive computation...
Core 30: Starting intensive computation...
Core 31: Starting intensive computation...
Core 3: Completed. Counter=1209229201, Sum=2.8033135773977207E13
Core 11: Completed. Counter=1211769364, Sum=2.8121513705789645E13
Core 8: Completed. Counter=1199590272, Sum=2.7698620755711055E13
Core 2: Completed. Counter=1203021176, Sum=2.7817535389470645E13
Core 13: Completed. Counter=1190825648, Sum=2.7395612274237375E13
Core 31: Completed. Counter=1218795961, Sum=2.83664671475123E13
Core 21: Completed. Counter=1212393233, Sum=2.814323367875016E13
Core 19: Completed. Counter=1204196135, Sum=2.785829831628206E13
Core 12: Completed. Counter=1223621706, Sum=2.8535106632545105E13
Core 20: Completed. Counter=1223951172, Sum=2.854663222930776E13
Core 26: Completed. Counter=1209303550, Sum=2.803572122383962E13
Core 29: Completed. Counter=1225677066, Sum=2.8607033934887254E13
Core 0: Completed. Counter=1222882977, Sum=2.8509269567738004E13
Core 23: Completed. Counter=1209603905, Sum=2.8046166730119613E13
Core 24: Completed. Counter=1189998737, Sum=2.7367081901189492E13
Core 9: Completed. Counter=1215672477, Sum=2.8257492284361145E13
Core 25: Completed. Counter=996223257, Sum=2.0962532819551504E13
Core 30: Completed. Counter=1199697911, Sum=2.770234892705607E13
Core 5: Completed. Counter=1214359549, Sum=2.821172744882745E13
Core 28: Completed. Counter=1206475954, Sum=2.7937448955144168E13
Core 18: Completed. Counter=1213988582, Sum=2.8198801103505355E13
Core 4: Completed. Counter=1208161491, Sum=2.7996015452241723E13
Core 7: Completed. Counter=1173755242, Sum=2.6808657016430223E13
Core 22: Completed. Counter=1193089284, Sum=2.7473763708917047E13
Core 16: Completed. Counter=1216669588, Sum=2.8292265177977305E13
Core 6: Completed. Counter=1214497392, Sum=2.8216531091415805E13
Core 14: Completed. Counter=1202774004, Sum=2.7808962768959176E13
Core 27: Completed. Counter=1213167406, Sum=2.817019424997135E13
Core 17: Completed. Counter=1204046012, Sum=2.785308898096554E13
Core 1: Completed. Counter=1208010559, Sum=2.7990769428434086E13
Core 15: Completed. Counter=1215303167, Sum=2.8244616717065184E13
Core 10: Completed. Counter=1215761769, Sum=2.8260605640605434E13

CPU stress test completed in 30043ms
All 32 cores were loaded for approximately 30 seconds

Note: Would be interesting to try this out on Mac & Windows later.


Children
  1. Q: Should you limit the concurrency of Dispatchers.Default? (A: No, for Linux multi core CPUs)

Backlinks