1. Introduction

In this quick tutorial, we’re going to create and execute threads in Kotlin.

Later on, we’ll discuss how to avoid it altogether, in favor of Kotlin Coroutines.

2. Creating Threads

Creating a thread in Kotlin is similar to doing so in Java.

We could either extend the Thread class (though it isn’t recommended due to Kotlin doesn’t support multiple inheritances):

class SimpleThread: Thread() {
    public override fun run() {
        println("${Thread.currentThread()} has run.")
    }
}

Or we can implement the Runnable interface:

class SimpleRunnable: Runnable {
    public override fun run() {
        println("${Thread.currentThread()} has run.")
    }
}

And in the same way, we do in Java, we can execute it by calling the start() method:

val thread = SimpleThread()
thread.start()
        
val threadWithRunnable = Thread(SimpleRunnable())
threadWithRunnable.start()

Alternatively, like Java 8, Kotlin supports SAM Conversions, therefore we can take advantage of it and pass a lambda:

val thread = Thread {
    println("${Thread.currentThread()} has run.")
}
thread.start()

2.2. Kotlin thread() Function

Another way is to consider the function thread() that Kotlin provides:

fun thread(
  start: Boolean = true, 
  isDaemon: Boolean = false, 
  contextClassLoader: ClassLoader? = null, 
  name: String? = null, 
  priority: Int = -1, 
  block: () -> Unit
): Thread

With this function, a thread can be instantiated and executed simply by:

thread(start = true) {
    println("${Thread.currentThread()} has run.")
}

The function accepts five parameters:

  • start – To run immediately the thread
  • isDaemon – To create the thread as a daemon thread
  • contextClassLoader – A class loader to use for loading classes and resources
  • name – To set the name of the thread
  • priority – To set the priority of the thread

3. Kotlin Coroutines

It’s tempting to think that spawning more threads can help us execute more tasks concurrently. Unfortunately, that’s not always true.

Creating too many threads can actually make an application underperform in some situations; threads are objects which impose overhead during object allocation and garbage collection.

To overcome these issues, Kotlin introduced a new way of writing asynchronous, non-blocking code; the Coroutine.

Similar to threads, coroutines can run in concurrently, wait for, and communicate with each other with the difference that creating them is way cheaper than threads.

3.1. Coroutine Context

Before presenting the coroutine builders that Kotlin provides out-of-the-box, we have to discuss the Coroutine Context.

Coroutines always execute in some context that is a set of various elements.

The main elements are:

  • Job – models a cancellable workflow with multiple states and a life-cycle that culminates in its completion
  • Dispatcher – determines what thread or threads the corresponding coroutine uses for its execution. With the dispatcher, we can confine coroutine execution to a specific thread, dispatch it to a thread pool, or let it run unconfined

We’ll see how to specify the context while we describe the coroutines in the next stages.

3.2. launch

The launch function is a coroutine builder that starts a new coroutine without blocking the current thread and returns a reference to the coroutine as a Job object:

runBlocking {
    val job = launch(Dispatchers.Default) {  
        println("${Thread.currentThread()} has run.") 
    }
}

It has two optional parameters:

  • context – The context in which the coroutine is executed, if not defined, it inherits the context from the CoroutineScope it is being launched from
  • start – The start options for the coroutine. By default, the coroutine is immediately scheduled for execution

Note that the above code is executed into a shared background pool of threads because we have used Dispatchers.Default which launches it in the GlobalScope.

Alternatively, we can use GlobalScope.launch which uses the same dispatcher:

val job = GlobalScope.launch {
    println("${Thread.currentThread()} has run.")
}

When we use Dispatchers.Default or GlobalScope.launch we create a top-level coroutine. Even though it is light-weight, it still consumes some memory resources while it runs.

Instead of launching coroutines in the GlobalScope, just like we usually do with threads (threads are always global), we can launch coroutines in the specific scope of the operation we are performing:

runBlocking {
    val job = launch {
        println("${Thread.currentThread()} has run.")
    }
}

In this case, we start a new coroutine inside the runBlocking coroutine builder (which we’ll describe later) without specifying the context. Thus, the coroutine will inherit runBlocking‘s context.

3.3. async

Another function that Kotlin provides to create a coroutine is async.

The async function creates a new coroutine and returns a future result as an instance of Deferred<T>:

val deferred = async {
    return@async "${Thread.currentThread()} has run."
}

deferred is a non-blocking cancellable future that describes an object that acts as a proxy for a result that is initially unknown.

Like launch, we can specify a context in which to execute the coroutine as well as a start option:

val deferred = async(Dispatchers.Unconfined, CoroutineStart.LAZY) {
    println("${Thread.currentThread()} has run.")
}

In this case, we’ve launched the coroutine using the Dispatchers.Unconfined which starts coroutines in the caller thread but only until the first suspension point. 

Note that Dispatchers.Unconfined is a good fit when a coroutine does not consume CPU time nor updates any shared data.

In addition, Kotlin provides Dispatchers.IO that uses a shared pool of on-demand created threads:

val deferred = async(Dispatchers.IO) {
    println("${Thread.currentThread()} has run.")
}

Dispatchers.IO is recommended when we need to do intensive I/O operations.

3.4. runBlocking

We had an earlier look at runBlocking, but now let’s talk about it in more depth.

runBlocking is a function that runs a new coroutine and blocks the current thread until its completion.

By way of example in the previous snippet, we launched the coroutine but we never waited for the result.

In order to wait for the result, we have to call the await() suspend method:

// async code goes here

runBlocking {
    val result = deferred.await()
    println(result)
}

await() is what’s called a suspend function. Suspend functions are only allowed to be called from a coroutine or another suspend function. For this reason, we have enclosed it in a runBlocking invocation.

We use runBlocking in main functions and in tests so that we can link blocking code to other written in suspending style.

In a similar fashion as we did in other coroutine builders, we can set the execution context:

runBlocking(newSingleThreadContext("dedicatedThread")) {
    val result = deferred.await()
    println(result)
}

Note that we can create a new thread in which we could execute the coroutine. However, a dedicated thread is an expensive resource. And, when no longer needed, we should release it or even better reuse it throughout the application.

4. Conclusion

In this tutorial, we learned how to execute asynchronous, non-blocking code by creating a thread.

As an alternative to the thread, we’ve also seen how Kotlin’s approach to using coroutines is simple and elegant.

As usual, all code samples shown in this tutorial are available over on Github.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.