Concurrency in Kotlin

Introduction

Concurrency simply refers to the quality of being concurrent—that is, to occur at the same time as another event. Programming-wise, we are enabling our central processing unit (C.P.U.) to manage numerous jobs or programs concurrently. We will examine concurrency in Kotlin in detail in this article.

Concurrency in Kotlin

About Concurrency in Kotlin

Concurrency is a fundamental concept in modern software development that enables multiple tasks to execute simultaneously, leading to improved performance and responsiveness. In Kotlin, a powerful programming language for the JVM, concurrency can be achieved using multithreading and synchronization techniques. This article aims to provide a comprehensive understanding of these concepts and how they can be applied in Kotlin.

Multithreading in Kotlin

Multithreading is a core aspect of achieving concurrency in Kotlin. It involves executing multiple threads simultaneously, where each thread represents an independent unit of execution. Kotlin provides built-in support for multithreading through the `Thread` class. Here's an overview of how multithreading can be implemented in Kotlin:

  1. Creating and Starting Threads: In Kotlin, threads can be created by extending the `Thread` class or implementing the `Runnable` interface. Once created, a thread can be started using the `start()` method, which invokes the `run()` method defined within the thread.
  2. Thread Synchronization: When multiple threads access shared resources, synchronization becomes crucial to prevent race conditions and ensure data consistency. Kotlin offers synchronization mechanisms such as locks and semaphores. The `synchronized` keyword can be used to create synchronized blocks that ensure only one thread can access a critical section of code at a time.
  3. Thread Communication: Threads often need to communicate with each other to coordinate their actions. Kotlin provides various mechanisms for thread communication, such as wait-notify signaling using the `wait()`, `notify()`, and `notifyAll()` methods.
  4. Thread Safety: Writing thread-safe code is essential to prevent concurrency issues. Kotlin provides atomic variables and concurrent data structures, such as `AtomicInteger` and `ConcurrentHashMap`, which ensure safe access and modification of shared data without the need for explicit synchronization.

Example

Let's examine a Kotlin example of multi-threading and its operation.

import kotlin.concurrent.thread

fun main() {
    println("Main thread started")
    
    // Create a new thread using the Thread class
    val thread1 = Thread {
        for (i in 1..5) {
            println("Thread 1: Count $i")
            Thread.sleep(1000) // Sleep for 1 second
        }
    }
    
    // Start the thread
    thread1.start()
    
    // Continue with the main thread
    for (i in 1..5) {
        println("Main thread: Count $i")
        Thread.sleep(500) // Sleep for 0.5 seconds
    }
    
    println("Main thread finished")
}

Here we are running two threads simultaneously, one by creating a new Thread instance using a lambda expression. and one on the Main thread. The output below shows that both functions are executing independently and printing the output without interfering with other processes.

Output

Synchronization

Synchronization is a key concept in concurrent programming that ensures proper coordination and access to shared resources. In Kotlin, synchronization can be achieved using various mechanisms, including:

  1. Locks: Kotlin provides the `Lock` interface and its implementations, such as `ReentrantLock`, for explicit locking. Locks allow threads to acquire exclusive access to a shared resource and release it when they're done. Locks offer more flexibility than the `synchronized` keyword by allowing advanced features like fairness, multiple condition variables, and tryLock.
  2. Semaphores: Semaphores are synchronization primitives that control access to a certain number of permits. In Kotlin, the `Semaphore` class can be used to limit the number of threads accessing a particular resource simultaneously. Threads can acquire and release permits using the `acquire()` and `release()` methods.
  3. Thread-safe Collections: Kotlin provides thread-safe collections, such as `ConcurrentHashMap` and `ConcurrentLinkedQueue`, which can be used in multithreaded scenarios without requiring explicit synchronization. These collections offer atomic operations and handle concurrent access internally.
  4. Atomic Variables: Kotlin's `Atomic` classes, such as `AtomicInteger` and `AtomicBoolean`, provide atomic operations on variables without the need for explicit synchronization. Atomic variables ensure that updates are performed atomically and prevent race conditions.

Example

Consider an example where we have multiple threads, and each thread is updating or increasing the count of a variable. We want to access the count only after all the operations on threads in done, and no threads are running any operations on the count variable.

class Counter {
    private var count = 0

    // Synchronized method to increment the count
    @Synchronized
    fun increment() {
        count++
    }

    // Synchronized method to get the current count
    @Synchronized
    fun getCount(): Int {
        return count
    }
}

fun main() {
    val counter = Counter()

    // Create multiple threads to increment the counter
    val threads = List(5) {
        Thread {
            for (i in 1..1000) {
                counter.increment()
            }
        }
    }

    // Start the threads
    threads.forEach { it.start() }

    // Wait for all threads to finish
    threads.forEach { it.join() }

    println("Final count: ${counter.getCount()}")
}

Here we are creating a list of 5 threads which is going to increase the count. Each thread is running from 1 to 1000, increasing the count of variables by 1000, and like this, we will finally receive a 5000 value of count at the end. Now By using the @Synchronized annotation, we ensure that only one thread can access the increment() and getCount() methods at a time. This prevents concurrent modification of the count variable and maintains data integrity. We use the join() method on each thread to wait for all threads to finish before proceeding. Finally, we print the final count using the getCount() method of the Counter class.

Output

Coroutines

Now there is a smarter way to handle the Concurrency in Kotlin, which is Coroutines, so what the hack coroutines are, let's see all this in detail.

Consider Coroutines as a lighter version of threads, which allows the execution of a block of code to be suspended & then resumed later. Coroutines come in handy when we need to do actions in Android applications repeatedly that can be completed in the meantime of user engagement.

Let's understand some basic terminology used when using coroutines in Kotlin.

Coroutines: Coroutines are made up of the two words co and routine, which stand for "cooperative routines" or "cooperative tasks."

Suspending function: This is similar to a standard Kotlin function with the added ability to suspend for a while and restart at a later time.

suspend fun doAsyncWork() {
    // Asynchronous work here
}

// Inside a coroutine
launch {
    doAsyncWork()
}

launch: This function is a coroutine builder provided by the Kotlin Coroutines library that's used to launch a new coroutine.

launch {
    doAsyncWork()
}

Async: This function returns a value when a coroutine is completed or finished.

Parallel Decomposition: breaking problems into small pieces that can be solved in parallel.

Coroutine Scope: To launch a Coroutine, a scope is needed. These scopes define the basic property of the coroutines. The most common scope is the CoroutineScope associated with a particular coroutine builder. You can also specify a CoroutineScope explicitly.

val scope = CoroutineScope(Dispatchers.Default)
scope.launch {
    // Coroutine code here
}

Coroutine Context and Dispatchers: Coroutines run in a specific environment known as context, which has a Coroutine dispatcher. The Dispatcher determines which thread or thread pool the coroutine will run on. The most common dispatchers are Dispatchers.Main (for UI interactions on Android), Dispatchers.IO (for I/O-bound operations), and Dispatchers.Default (for CPU-bound operations). You can specify a dispatcher when launching a coroutine.

launch(Dispatchers.IO) {
    // Coroutine code here
}

Cancellation and Structured Concurrency: Coroutines support structured concurrency, which means that they can be canceled more reliably and automatically than threads. When a parent coroutine is canceled, all its child coroutines are automatically canceled as well. This helps prevent resource leaks and ensures a more predictable cleanup of resources.

Example

Let's try a simple Kotlin example of a coroutine where we only launch fake coroutines to retrieve data from the server and display it in the user interface when the data is received.

import kotlinx.coroutines.*

fun main() {
    println("Main thread started")

    // Launch a coroutine in the GlobalScope
    GlobalScope.launch {
        val data = fetchDataFromServer()
        displayData(data)
    }

    println("Main thread continues")

    // Keep the program running to allow coroutines to finish
    runBlocking {
        delay(2000) // Wait for 2 seconds
    }

    println("Main thread finished")
}

suspend fun fetchDataFromServer(): String {
    delay(1000) // Simulate network delay
    return "Fetched data from server"
}

fun displayData(data: String) {
    println("Data received: $data")
}

Firstly we printed in the console that we are running the program in the main thread. Then we launch a coroutine using the GlobalScope.launch function. The coroutine's body contains code that simulates fetching data from a server using the fetchDataFromServer function and then displaying the data using the displayData function.

To keep the program running for a while, we employ runBlocking. Otherwise, the program might end before the coroutine has a chance to finish, therefore, this is required. The launched coroutine is still running in the background during this time.

We mimic network latency using the delay function inside the fetchDataFromServer function. The term suspend denotes the fact that this function can be used inside of a coroutine and that it can pause the execution of the coroutine without blocking the thread. When data is received, we display a message to signal the end of the main thread.

You can see that the main thread starts, initiates a coroutine, continues its execution, waits for the coroutine to finish, and then completes when you run this code. This exemplifies how coroutines, as opposed to conventional callback-based techniques or using raw threads, allow you to build asynchronous code that is more readable and maintainable.

Output

Summary

Concurrency is a crucial aspect of modern software development, and Kotlin offers powerful tools and techniques to achieve it. By leveraging multithreading and synchronization mechanisms, developers can design efficient and thread-safe applications. Understanding the concepts of multithreading, synchronization, and thread safety is essential for writing robust concurrent code in Kotlin. With the knowledge gained from this article, developers can embark on building highly concurrent and performant applications using Kotlin's concurrency features.

Remember, mastering concurrency requires practice and a solid understanding of the principles involved. By continuously exploring and experimenting with Kotlin's concurrency mechanisms, developers can enhance their skills in writing efficient and scalable concurrent applications. Hope this has been a helpful guide for you, do not forget to write your views about this article in the comments. Thank you!


Similar Articles