.wait()
, .sleep()
, and .delay()
Multithreading programming is one of those taboo subjects that not many developers enjoy talking about. The complexity of the topic has driven away even experienced programmers for as long as it has been helping us solve problems.
This had been particularly true for the Android platform, especially back when we were predominantly using Java. Between the juggling of different threads, its Handler
s and Runner
s, there was little time for sanity. And let’s not mention the memory leaks.
When the Android OS was introduced to the world of coroutines by the new programming language Kotlin, everything changed. The practice of multithreading programming stopped feeling as intimidating or foreign as it did before. Kotlin and its coroutines allowed for developers to write multithreading code in a cleaner and more efficient way. Check out this article to learn more information on what Kotlin coroutines are and how to use them.
Kotlin coroutines introduced a new set of keywords and APIs that made multithreading patterns easier to read and ultimately understand. Keywords such as suspend
and runBlocking
allow for a more human-readable way of understanding what these multithreading APIs do, and allow devs to better establish their expectations. For a deeper explanation on the two keywords mentioned above and their usage, check out this article.
In this article, I will explore three individual functions that are commonly used in multithreading programming while working in Kotlin: wait()
, sleep()
, and delay()
. As you may have noticed, not all of these three functions actually belong to the Kotlin programming language, but instead we’ll approach this inquiry into the particularities of the multithreading paradigm from a broader perspective.
Jump ahead:
Let’s start from the very bottom. The .wait()
function pertains to the the low-level Object
class not from Kotlin, but from the Java programming language. Object#wait()
is meant to be called onto any kind of object in order to instruct the running thread to wait indefinitely. As the Java official documentation illustrates, calling .wait()
behaves the same way as the call wait(0)
, or it causes the current thread to wait until another thread calls .notify()
or .notifyAll()
on the same object.
Let’s elaborate further with a simple example. Assume we have the object obj
in our running working thread thread1
, and we call wait()
on it:
// thread1 obj.wait()
This would effectively sleep the thread until it is either notified, as it was explained above, or otherwise interrupted. In order to notify thread1
to continue execution from thread2
, we would call the notify()
function on the same obj
instance:
// thread2 obj.notify()
This pattern is particularly useful when we want a thread to wait when hitting a specific part of the code. The object to which we wait — in our example above, the obj
variable — serves as a lock that conducts the thread’s execution through a timeout that may only be lifted by another thread’s action.
In other words, thread1
will be blocked and allow for asynchronous execution of other thread(s) — in our example above, we only run thread2
simultaneously — until thread1
is allowed to continue its execution.
Let’s go over one more example, which we’ll use to illustrate the differences of this function with respect to our other two protagonists:
import java.util.concurrent.TimeUnit import kotlin.test.* import kotlinx.coroutines.* val scope = CoroutineScope(Dispatchers.Default) val lock = Object() fun main() { println("We're testing .wait()!") runBlocking(Dispatchers.Default) { launch(Dispatchers.IO) { testWaitThread2() } testWaitThread1() } } fun testWaitThread1() = synchronized(lock) { lock.wait() println("Print second") } fun testWaitThread2() = synchronized(lock) { println("Print first") lock.notify() }
There are two different sleep functions available to the Kotlin programming language: Thread#sleep()
and TimeUnit#sleep()
. Both of these functions essentially perform the same thing, as TimeUnit#sleep()
will call Thread#sleep()
as part of its execution. Therefore, both of these methods will sleep a thread, or cease its execution temporarily, by the specified amount of time.
The main difference between these options is that the TimeUnit
equivalent of the sleep function will first verify that the unit received is positive, and only call Thread#sleep()
after that is cleared out. This means that the TimeUnit
version of the sleep function is safer to use because it will not throw an IllegalArgumentException
, as the Thread
version of the function will do with negative numbers.
The second advantage of the TimeUnit
implementation of sleep is how much more readable it is. The following example will show usage of both kinds of sleep functions, and it will demonstrate how the Thread
equivalent of it can become very hard to read too easily:
// 1 second Thread.sleep(1000) TimeUnit.SECONDS.sleep(1) // 120 seconds Thread.sleep(120000) TimeUnit.SECONDS.sleep(120) // 1 minute Thread.sleep(60000) TimeUnit.MINUTES.sleep(1) // 19 minute Thread.sleep(540000) TimeUnit.MINUTES.sleep(19) // 1 hour Thread.sleep(3600000) TimeUnit.HOURS.sleep(1)
Because both of these functions do the same and send the thread to sleep, we find a useful tool for asynchronous programming for whenever we want to control the order of operations. For example, the following program will demonstrate how creating two different threads and calling sleep on them simultaneously, we can control who goes first based on the duration of their sleep functions:
import java.util.concurrent.TimeUnit import kotlin.test.* import kotlinx.coroutines.* val scope = CoroutineScope(Dispatchers.Default) fun main() { println("We're testing .sleep()!") runBlocking(Dispatchers.Default) { launch { testSleepThread() } launch { testSleepTimeUnit() } } } suspend fun testSleepThread() { Thread.sleep(5000) // 5000 ms = 5 seconds println("Print second") } suspend fun testSleepTimeUnit() { TimeUnit.SECONDS.sleep(3) println("Print first") }
Last but not least, .delay()
is the only function from today’s cast that belongs to the Kotlin programming language. Differently from the last two protagonists, delay()
can only be called from inside a coroutine.
This function delays the given coroutine for a given time without blocking a thread, as the Kotlin official documentation explains, and it resumes after a specified time in the function parameter. This suspending function is cancellable, which means that we can ask the coroutine to continue execution at any point in time.
From the perspective of asynchronous design, the delay function behaves very much the same as either of the sleep functions, as it halts the execution of the given component of concurrent programming for a predefined amount of time.
We’ll use one last example to demonstrate the similarities between the delay()
function and the predeceasing sleep()
functions with a slight modification. Notice that we arrive at the exact same conclusion, with almost the exact same code:
import java.util.concurrent.TimeUnit import kotlin.test.* import kotlinx.coroutines.* val scope = CoroutineScope(Dispatchers.Default) val lock = Object() fun main() { println("We're testing .delay()!") runBlocking(Dispatchers.Default) { launch { testDelayThread2() } launch { testDelayThread1() } } } suspend fun testDelayThread1() { delay(5000) println("Print second") } suspend fun testDelayThread2() { delay(3000) println("Print first") }
.sleep()
, .wait()
, and .delay()
Now that we’ve explored each one of the main functions of this inquiry, let’s switch gears and analyze the main differences and similarities between each of them.
Evidently, the first thing that stands out is the similarity between sleep
and delay
. Both of these functions halt the running thread or coroutine for a given amount of time. Also, in their simplest forms, both of these functions accept a single parameter that specifies the duration of the given execution freeze, and both of them take that parameter in milliseconds.
Nevertheless, and as it has been mentioned before, the sleep
function exclusively applies to threads, while the delay
function instead freezes the execution of a coroutine, which does not guarantee whether the thread will be blocked or not.
Moreover, it is obvious that the wait
function stands on different grounds. It uses an object as a locking mechanism, where the calling thread does not continue execution until it is given the signal to do so. This means that while their counter options halt execution for a finite and defined amount of time, the wait
function may keep the thread locked, or frozen, for an indefinite time.
The introduction of the Kotlin coroutines into the multithreading world of Java added both an extra layer of complications and a brand new set of solutions. Today we’ve explored a small corner of the product of that through the .wait()
, sleep()
, and .delay()
functions.
We’ve seen how these functions can be used to control the flow and order of execution of different streams of information, and how this can be advantageous when dealing with multithreading and asynchronous programming alike.
LogRocket is an Android monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your Android apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your Android apps — try LogRocket for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.