Concurrency and Multithreading in Kotlin: From Theory to Practice
Modern applications require fast and responsive performance. Whether it's loading data from the network, processing large arrays, or working with the user interface — concurrency and multithreading are essential. Kotlin, being a modern language for JVM, Android, and multiplatform projects, offers flexible tools for solving these problems. In this article, we will break down key approaches: from classic threads to lightweight coroutines, and show how to write efficient code.
1. Threads in Kotlin: The Classic Approach
Threads are a basic mechanism for parallel execution inherited from Java. In Kotlin, you can create threads the same way as in Java, but with a more concise syntax.
A simple example of starting a thread:
fun main() { thread { println("Thread started: ${Thread.currentThread().name}") Thread.sleep(1000) println("Thread finished") } println("Main thread: ${Thread.currentThread().name}") Thread.sleep(2000)}However, threads have disadvantages: they are heavyweight (each thread consumes about 1 MB of memory), their number is limited, and context switching is expensive. For modern applications with thousands of parallel tasks, threads are not the best choice. This is where coroutines come to the rescue.
2. Coroutines: A Lightweight Alternative
Coroutines are the main feature of Kotlin for concurrency. They work on top of threads but use fewer resources. One thread can execute thousands of coroutines, suspending and resuming them without blocking.
To get started, add the dependency to build.gradle.kts:
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")}Example of a simple coroutine:
import kotlinx.coroutines.*
fun main() = runBlocking { launch { delay(1000L) // non-blocking delay println("World from coroutine!") } println("Hello from main coroutine") delay(2000L)}Here, launch starts a new coroutine, and delay suspends it without blocking the thread. runBlocking is used to block the main thread until all coroutines complete (typically used only in main and tests).
2.1. Suspend Functions
The suspend keyword allows you to pause the execution of a function without blocking the thread. This is the foundation of asynchronous programming in Kotlin.
suspend fun fetchUser(): String { delay(500L) // simulating a network request return "User123"}
fun main() = runBlocking { val user = fetchUser() println("User received: $user")}2.2. Async and Deferred for Parallel Tasks
If you need to execute several tasks in parallel and wait for their results, use async and Deferred:
fun main() = runBlocking { val time = measureTimeMillis { val user = async { fetchUser() } val profile = async { fetchProfile() } println("Results: ${user.await()}, ${profile.await()}") } println("Execution time: $time ms")}
suspend fun fetchUser(): String { delay(500) return "User"}
suspend fun fetchProfile(): String { delay(500) return "Profile"}Both tasks execute in parallel, reducing the total time.
3. Coroutine Dispatchers
Dispatchers determine which threads the coroutines will run on. The main types are:
Dispatchers.Default— for CPU-intensive tasks (default).Dispatchers.IO— for I/O operations (network, files).Dispatchers.Main— for the UI thread (Android, JavaFX).