Simple and beginner-friendly Kotlin code examples to show the different behavior of using kotlinx.coroutines.delay() and Thread.sleep()
This is part of the Kotlin coroutines series:
- Part 1 - Kotlin Coroutines Basics - Simple Android App Demo
- Part 2 - kotlinx.coroutines.delay() vs Thread.sleep()
- Part 3 - GlobalScope vs viewModelScope vs lifecycleScope vs rememberCoroutineScope
- Part 4 - launchWhenCreated() vs launchWhenStarted() vs launchWhenResumed() vs repeatOnLifeCycle()
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()
.
Originally published at https://vtsen.hashnode.dev.
Top comments (0)