Search code examples
multithreadingkotlinconcurrencykotlin-coroutines

Coroutines and number of threads: Thread.sleep() vs delay()


I am testing coroutines using a custom Dispacher with a thread pool of size = 1.

const val customThreadPoolSize = 1 
val customDispatcher = Executors.newFixedThreadPool(customThreadPoolSize).asCoroutineDispatcher()

I created 2 functions, each containing 5 coroutines that simulate doing some work.

  1. In fun1 I used Thread.sleep() to represent long-running operations.
  2. In fun2 I used delay() to represent long-running operations.

Thus, fun1 will look like:

fun1() {
    println("Current thread is: ${Thread.currentThread().name}")
    println("Current number of threads is: ${Thread.activeCount()}")

    GlobalScope.launch(customDispatcher) {
        println("(Coroutine1) Current thread is: ${Thread.currentThread().name}")
        println("(Coroutine1) Current number of threads is: ${Thread.activeCount()}")
        for (i in 0..3) {
            println("(Coroutine1) Doing work:$i")
            Thread.sleep(1000) // Doing work on worker thread. Totally blocks the thread
        }
        println("(Coroutine1) Coroutine1 finished work")
    }

    GlobalScope.launch(customDispatcher) {
        println("(Coroutine2) Current thread is: ${Thread.currentThread().name}")
        println("(Coroutine2) Current number of threads is: ${Thread.activeCount()}")
        for (i in 0..3) {
            println("(Coroutine2) Doing work:$i")
            Thread.sleep(1000) // Doing work on worker thread. Totally blocks the thread
        }
        println("(Coroutine2) Coroutine2 finished work")
    }

    GlobalScope.launch(customDispatcher) {
        println("(Coroutine3) Current thread is: ${Thread.currentThread().name}")
        println("(Coroutine3) Current number of threads is: ${Thread.activeCount()}")
        for (i in 0..3) {
            println("(Coroutine3) Doing work:$i")
            Thread.sleep(1000) // Doing work on worker thread. Totally blocks the thread
        }
        println("(Coroutine3) Coroutine3 finished work")
    }

    GlobalScope.launch(customDispatcher) {
        println("(Coroutine4) Current thread is: ${Thread.currentThread().name}")
        println("(Coroutine4) Current number of threads is: ${Thread.activeCount()}")
        for (i in 0..3) {
            println("(Coroutine4) Doing work:$i")
            Thread.sleep(1000) // Doing work on worker thread. Totally blocks the thread
        }
        println("(Coroutine4) Coroutine4 finished work")
    }

    println("Current thread is: ${Thread.currentThread().name}")
    println("Current number of threads is: ${Thread.activeCount()}")

    GlobalScope.launch(customDispatcher) {
        println("(Coroutine5) Current thread is: ${Thread.currentThread().name}")
        println("(Coroutine5) Current number of threads is: ${Thread.activeCount()}")
        for (i in 0..3) {
            println("(Coroutine5) Doing work:$i")
            Thread.sleep(1000) // Doing work on worker thread. Totally blocks the thread
        }
        println("(Coroutine5) Coroutine5 finished work")
    }

    println("Current thread is: ${Thread.currentThread().name}")
    println("Current number of threads is: ${Thread.activeCount()}")

    for (i in 0..3) {
        println("(Main thread) Doing work:$i")
        Thread.sleep(1000) // Doing work on main thread
    }
    println("(Main thread) Main thread finished work")
    customDispatcher.close()
}

When running fun1 the console prints:

Current thread is: main
Current number of threads is: 2
Current thread is: main
Current number of threads is: 3
Current thread is: main
Current number of threads is: 3
(Main thread) Doing work:0
(Coroutine1) Current thread is: pool-1-thread-1
(Coroutine1) Current number of threads is: 3
(Coroutine1) Doing work:0
(Main thread) Doing work:1
(Coroutine1) Doing work:1
(Coroutine1) Doing work:2
(Main thread) Doing work:2
(Coroutine1) Doing work:3
(Main thread) Doing work:3
(Coroutine1) Coroutine1 finished work
(Main thread) Main thread finished work
(Coroutine2) Current thread is: pool-1-thread-1
(Coroutine2) Current number of threads is: 3
(Coroutine2) Doing work:0
(Coroutine2) Doing work:1
(Coroutine2) Doing work:2
(Coroutine2) Doing work:3
(Coroutine2) Coroutine2 finished work
(Coroutine3) Current thread is: pool-1-thread-1
(Coroutine3) Current number of threads is: 3
(Coroutine3) Doing work:0
(Coroutine3) Doing work:1
(Coroutine3) Doing work:2
(Coroutine3) Doing work:3
(Coroutine3) Coroutine3 finished work
(Coroutine4) Current thread is: pool-1-thread-1
(Coroutine4) Current number of threads is: 3
(Coroutine4) Doing work:0
(Coroutine4) Doing work:1
(Coroutine4) Doing work:2
(Coroutine4) Doing work:3
(Coroutine4) Coroutine4 finished work
(Coroutine5) Current thread is: pool-1-thread-1
(Coroutine5) Current number of threads is: 3
(Coroutine5) Doing work:0
(Coroutine5) Doing work:1
(Coroutine5) Doing work:2
(Coroutine5) Doing work:3
(Coroutine5) Coroutine5 finished work

As shown above, the maximum number of threads working is 3.

On the other hand when running fun2, the console prints:

Current thread is: main
Current number of threads is: 2
Current thread is: main
Current number of threads is: 3
Current thread is: main
Current number of threads is: 3
(Main thread) Doing work:0
(Coroutine1) Current thread is: pool-1-thread-1
(Coroutine1) Current number of threads is: 3
(Coroutine1) Doing work:0
(Coroutine2) Current thread is: pool-1-thread-1
(Coroutine2) Current number of threads is: 4
(Coroutine2) Doing work:0
(Coroutine3) Current thread is: pool-1-thread-1
(Coroutine3) Current number of threads is: 4
(Coroutine3) Doing work:0
(Coroutine4) Current thread is: pool-1-thread-1
(Coroutine4) Current number of threads is: 4
(Coroutine4) Doing work:0
(Coroutine5) Current thread is: pool-1-thread-1
(Coroutine5) Current number of threads is: 4
(Coroutine5) Doing work:0
(Main thread) Doing work:1
(Coroutine1) Doing work:1
(Coroutine2) Doing work:1
(Coroutine3) Doing work:1
(Coroutine4) Doing work:1
(Coroutine5) Doing work:1
(Main thread) Doing work:2
(Coroutine1) Doing work:2
(Coroutine2) Doing work:2
(Coroutine3) Doing work:2
(Coroutine4) Doing work:2
(Coroutine5) Doing work:2
(Main thread) Doing work:3
(Coroutine1) Doing work:3
(Coroutine2) Doing work:3
(Coroutine3) Doing work:3
(Coroutine4) Doing work:3
(Coroutine5) Doing work:3

In this case, the maximum number of threads working is 4.


Now, from where did that extra thread come from and what is it being used for?


Solution

  • delay()'s implementation varies based on what the current Dispatcher is.

    The default IO and Default dispatchers do not define their own delay implementation, and therefore use the default delay implementation.

    In apps that have a Dispatchers.Main, the main thread loop is used to schedule delays for the default delay implementation.

    But without a Dispatchers.Main, as is probably the case in your test code, the default delay implementation uses a shared Executor for scheduling the delays and callbacks to the dispatchers. This Executor would be the source of your extra thread.

    I just browsed the JVM coroutines source code to find this information. The behavior is likely quite different on JS and other platforms.