kotlinx.coroutines.delay() vs Thread.sleep()

Simple and beginner-friendly Kotlin code examples to show the different behavior of using kotlinx.coroutines.delay() and Thread.sleep()

kotlinx.coroutines.delay() vs Thread.sleep()

This is part of the Kotlin coroutines series:

kotlinx.coroutines.delay() is a suspend function. It doesn't block the current thread. Thread.sleep() blocks the current thread. It means other code in this thread won't be executed until Thread.sleep() is exited.

Example 1 - kotlinx.coroutines.delay()

fun main(args: Array<String>) {
    runBlocking {
            run()
        }
    }
}

suspend fun run() {
    coroutineScope {
        val timeInMillis = measureTimeMillis {
            val mainJob = launch {
                //Job 0
                launch {
                    print("A->")
                    delay(1000)
                    print("B->")
                }
                //Job 1
                launch {
                    print("C->")
                    delay(2000)
                    print("D->")
                }
                //Job 2
                launch {
                    print("E->")
                    delay(500)
                    print("F->")
                }

                //Main job
                print("G->")
                delay(1500)
                print("H->")
            }

            mainJob.join()
        }

        val timeInSeconds =
            String.format("%.1f", timeInMillis/1000f)
        print("${timeInSeconds}s")
   }
}

Main job will run first and then will be suspended by delay() suspend function, followed by Job 0 -> Job 1 -> Job 2. All jobs are suspended and kicked off at around the same time. Then, the shortest delay() will be run first. The timeinSeconds to complete all the jobs should be the longest delay() which is 2 seconds.

The output looks like this:

G->A->C->E->F->B->H->D->2.0s

This is pretty easy to understand. What if we replace delay(2000) with Thread.Sleep(2000) for Job1?

Example 2 - Thread.sleep() on Dispatchers.Main

suspend fun run() {
    coroutineScope {
        val timeInMillis = measureTimeMillis {
            val mainJob = launch {
                //Job 0
                launch {
                    print("A->")
                    delay(1000)
                    print("B->")
                }
                //Job 1
                launch {
                    print("C->")
                    Thread.sleep(2000)
                    print("D->")
                }
                //Job 2
                launch {
                    print("E->")
                    delay(500)
                    print("F->")
                }

                //Main job
                print("G->")
                delay(1500)
                print("H->")
            }

            mainJob.join()
        }

        val timeInSeconds =
            String.format("%.1f", timeInMillis/1000f)
        print("${timeInSeconds}s")
   }
}

Similar to example 1 above, Main job will run first and suspended by delay() suspend function, followed by Job 0 → Job 1. Job 0 will be suspended. However, when Thread.sleep(2000) is run on Job 1, the thread will be blocked for 2 seconds. Job 2 at this time is not executed.

After 2 seconds, D will be printed out first, followed by E in Job 2. Job 2 then will be suspended. Because Main job and Job 0 are suspended less than 2 seconds, it will run immediately. Job 0 will run first because the suspend time is shorter.

After 0.5 seconds, Job 2 is resumed and completed. It will print out F.

Timestamp #1 (after 0 second)

  • Main job and Job 0 are started and suspended.

  • Job 1 is started and blocks the thread

Timestamp #2 (after 2 seconds)

  • Job 1 is done

  • Job 2 is started and suspended.

  • Job 0 and Main job are resumed and done.

Timestamp #3 (after 0.5 seconds)

  • Job 3 are resumed and done

So the total time consumes is around 2.5 seconds.

The output looks like this:

G->A->C->D->E->B->H->F->2.5s

Example 3 - Thread.sleep() on Dispatchers.Default/IO

Wait, what if run the run suspend function in background thread using Dispatchers.Default or Dispatchers.IO. For example:

runBlocking {
    withContext(Dispatchers.Default) {
        run()
    }
}

The output becomes like this:

A->C->G->E->F->B->H->D->2.0s

The output is similar to Example 1 above, where Thread.sleep() doesn't seem to block the thread! Why?

When Dispatchers.Default or Dispatchers.IO is used, it is backed by a pool of threads. Each time we call launch{}, a different worker thread is created / used.

For example, here are the worker threads being used:

  • Main job - DefaultDispatcher-worker-1

  • Job 0 - DefaultDispatcher-worker-2

  • Job 1 - DefaultDispatcher-worker-3

  • Job 2 - DefaultDispatcher-worker-4

To see which thread is currently running, you can use println("Run ${Thread.currentThread().name}")

So Thread.sleep() indeed blocks that thread, but only blocks the DefaultDispatcher-worker-3. The other jobs can still be continued to run since they're on different threads.

Timestamp #1 (after 0 second)

  • Main job, Job 0, Job 1 and Job 2 are started. Sequence can be random. See Note(1) below.

  • Main job, Job 0 and **Job2 ** are suspended.

  • **Job 1 ** blocks its own thread.

Timestamp #2 (after 0.5 second)

  • Job 2 is resumed and done.

Timestamp #3 (after 1 second)

  • Job 0 are resumed and done

Timestamp #4 (after 1.5 seconds)

  • Main job are resumed and done

Timestamp #5 (after 2 seconds)

  • Job 1 are resumed and done

Because each job runs on a different thread, the job can be started at different time. So the output of A, C, E, G could be random. Thus, you see the initiat job starting sequence is different than the one in Exampe 1 above.

When to use Thread.Sleep()?

Thread.Sleep() is almost useless because most of the time we don't want to block the thread. kotlinx.coroutines.delay() is recommended.

I personally use Thread.Sleep() to simulate long-running task that block the thread. It is useful to test whether I have put the long-running task into the background thread. If I run it from the main UI thread, the UI won't be responsive.

If I call this simulateBlockingThreadTask() in the main UI thread, it will block the main UI thread. The application will crash with non-responsive UI.

private suspend fun simulateBlockingThreadTask() {
        Thread.sleep(2000)
 }

However, if we switch the thread to background thread using kotlinx.coroutines.withContext(), calling this simulateBlockingThreadTask() from the main UI thread won't crash the application.

private suspend fun simulateBlockingThreadTask() {
    withContext(Dispatchers.Default) {
        Thread.sleep(2000)
    }
}

Remember to use yield()

In my previous example in Coroutines Basics blog post, I used yield() to break out the Thread.sleep() to basically allow the coroutine to be cancellable. It is generally a good practice not to block the UI thread for too long.

In the code example, I simulate both blocking and non-blocking thread tasks. The total running time is 400 milliseconds.

private suspend fun simulateLongRunningTask() {
    simulateBlockingThreadTask()
    simulateNonBlockingThreadTask()
}

private suspend fun simulateBlockingThreadTask() {
    repeat(10) {
        Thread.sleep(20)
        yield()
    }
}

private suspend fun simulateNonBlockingThreadTask() {
    delay(200)
}

Conclusion

Thread.sleep() blocks the thread and kotlinx.coroutines.delay() doesn't.

I use Thread.sleep() to test whether I have properly put the long-running task into background thread. Other than this, I can't think of any reasons we want to use Thread.sleep().

Did you find this article valuable?

Support Vincent Tsen by becoming a sponsor. Any amount is appreciated!