Concurrency and Parallelism in Kotlin

Concurrency and parallelism in Kotlin are two vital concepts in software development that enable executing multiple tasks simultaneously. While these terms are often used interchangeably, they represent distinct concepts.

Concurrency vs. Parallelism

Concurrency involves executing multiple tasks simultaneously, irrespective of whether they run on different processors. It breaks tasks into smaller pieces and executes them independently but doesn’t guarantee parallel execution. On the other hand, parallelism involves executing tasks simultaneously across multiple processors, achieving true parallel execution.

In this blog post, we’ll delve into concurrency and parallelism in Kotlin, implementing them using threads and coroutines.

1. Threads

Threads are a fundamental mechanism for achieving concurrency in Kotlin. They allow multiple tasks to run concurrently on a single CPU or on multiple CPUs simultaneously.

1.1 Thread Basics

Let’s start with a basic example of creating and running a thread in Kotlin:

Kotlin
import kotlin.concurrent.thread

fun main() {
    val thread = thread {
        println("Thread is running")
    }
    thread.join() // Wait for the thread to finish execution
}

In this example, we use thread from kotlin.concurrent to create a new thread that prints a message.

1.2 Synchronization and Thread Safety

Concurrency introduces challenges like data races, which can be mitigated using synchronization mechanisms such as locks. Here’s an example of synchronized access to a shared resource:

Kotlin
import kotlin.concurrent.thread

var counter = 0

fun main() {
    val threads = List(10) {
        thread {
            synchronized(this) {
                counter++
                println("Counter: $counter")
            }
        }
    }
    threads.forEach { it.join() } // Wait for all threads to finish
}

In this example, multiple threads increment a shared counter within a synchronized block to ensure thread safety.

2. Coroutines

Coroutines are a more advanced mechanism for achieving concurrency and parallelism in Kotlin. They offer a lightweight and flexible approach to asynchronous programming.

2.1 Coroutine Basics

Let’s create a simple coroutine that performs an asynchronous operation:

Kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        delay(1000L)
        println("Coroutine is running")
    }
    job.join() // Wait for the coroutine to finish execution
}

In this example, we use launch from kotlinx.coroutines to create a coroutine that prints a message after a delay.

2.2 Concurrent Programming with Coroutines

Coroutines enable concurrent programming, allowing multiple coroutines to run concurrently on a single thread. Consider fetching data from multiple sources concurrently:

Kotlin
import kotlinx.coroutines.*

suspend fun fetchData(url: String): String {
    // Simulate fetching data from URL
    delay(1000L)
    return "Data from $url"
}

fun main() = runBlocking {
    val urls = listOf("https://example.com/api1", "https://example.com/api2")
    val results = urls.map { url ->
        async {
            fetchData(url)
        }
    }
    results.forEach { println(it.await()) }
}

In this example, async creates multiple coroutines to fetch data from different URLs concurrently.

3. Real-World Examples

3.1 Downloading Images with Threads

Kotlin
import java.net.URL

fun main() {
    val urls = listOf(
        "https://example.com/image1.jpg",
        "https://example.com/image2.jpg",
        "https://example.com/image3.jpg",
    )
    val threads = urls.map {
        thread {
            val url = URL(it)
            val stream = url.openStream()
            // Code to process the downloaded image
        }
    }
    threads.forEach { it.start() }
    threads.forEach { it.join() }
}

In this example, threads are used to download images concurrently from different URLs.

3.2 Downloading Images with Coroutines

Kotlin
import kotlinx.coroutines.*
import java.net.URL

fun main() = runBlocking {
    val urls = listOf(
        "https://example.com/image1.jpg",
        "https://example.com/image2.jpg",
        "https://example.com/image3.jpg",
    )
    val deferred = urls.map {
        async {
            val url = URL(it)
            val stream = url.openStream()
            // Code to process the downloaded image
        }
    }
    deferred.awaitAll()
}

In this example, coroutines are used to download images concurrently from different URLs.

4. Coroutine Context

Coroutine context plays a crucial role in managing coroutines’ execution. Different dispatchers and contexts offer control over coroutine execution.

Kotlin
import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    launch(Dispatchers.Default) {
        println("Running in Default dispatcher")
        println("Current thread: ${Thread.currentThread().name}")
    }

    launch(Dispatchers.IO) {
        println("Running in IO dispatcher")
        println("Current thread: ${Thread.currentThread().name}")
    }

    launch(newSingleThreadContext("MyThread")) {
        println("Running in a single-threaded context")
        println("Current thread: ${Thread.currentThread().name}")
    }
}

In this example, we demonstrate different coroutine contexts and dispatchers in Kotlin.

Example: Web Scraping with Concurrent Requests

In this example, we’ll use concurrency to scrape multiple web pages concurrently.

Kotlin
import kotlinx.coroutines.*
import org.jsoup.Jsoup
import java.io.File

suspend fun fetchAndSaveWebPage(url: String, fileName: String) {
    val htmlContent = Jsoup.connect(url).get().outerHtml()
    File(fileName).writeText(htmlContent)
    println("Fetched and saved $url to $fileName")
}

fun main() = runBlocking {
    val urls = listOf(
        "https://example.com/page1",
        "https://example.com/page2",
        "https://example.com/page3"
    )

    val jobs = urls.mapIndexed { index, url ->
        launch {
            fetchAndSaveWebPage(url, "page_$index.html")
        }
    }

    jobs.forEach { it.join() }
}

Explanation:

  • We import kotlinx.coroutines.* for coroutines and org.jsoup.Jsoup for web scraping.
  • The fetchAndSaveWebPage function asynchronously fetches a web page using Jsoup and saves it to a file.
  • In main, we define a list of URLs to scrape concurrently.
  • We use coroutines to launch a job for each URL, calling fetchAndSaveWebPage for each.
  • Finally, we wait for all jobs to finish using join().

This example demonstrates how concurrency can speed up web scraping tasks by fetching multiple pages simultaneously.

Example: Image Processing with Parallelism

Let’s explore parallelism by processing multiple images concurrently.

Kotlin
import kotlinx.coroutines.*
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO

suspend fun processImage(inputPath: String, outputPath: String) {
    val inputFile = File(inputPath)
    val outputFile = File(outputPath)

    val image = ImageIO.read(inputFile)
    val processedImage = filterImage(image)

    ImageIO.write(processedImage, "jpg", outputFile)
    println("Processed image saved to $outputPath")
}

fun filterImage(image: BufferedImage): BufferedImage {
    // Apply image processing filters (e.g., resize, filter, etc.)
    return image
}

fun main() = runBlocking {
    val imagePaths = listOf(
        "input/image1.jpg",
        "input/image2.jpg",
        "input/image3.jpg"
    )

    val jobs = imagePaths.mapIndexed { index, inputPath ->
        launch {
            processImage(inputPath, "output/image_$index.jpg")
        }
    }

    jobs.forEach { it.join() }
}

Explanation:

  • We import kotlinx.coroutines.* for coroutines and Java’s image processing libraries.
  • processImage asynchronously reads an image, processes it (e.g., applying filters), and saves the processed image.
  • In main, we define a list of image paths to process in parallel.
  • Using coroutines, we launch a job for each image path to process it concurrently.
  • After all jobs finish processing, we continue execution.

This example illustrates how parallelism can speed up image processing tasks by leveraging multiple CPU cores.

Example: Concurrent API Requests

Let’s use concurrency to fetch data from multiple APIs concurrently.

Kotlin
import kotlinx.coroutines.*
import java.net.URL

suspend fun fetchDataFromApi(apiUrl: String): String {
    return URL(apiUrl).readText()
}

fun main() = runBlocking {
    val apiUrls = listOf(
        "https://api.example.com/data1",
        "https://api.example.com/data2",
        "https://api.example.com/data3"
    )

    val responses = apiUrls.map { apiUrl ->
        async {
            fetchDataFromApi(apiUrl)
        }
    }

    val data = responses.awaitAll()
    data.forEachIndexed { index, response ->
        println("Data from API $index: $response")
    }
}

Explanation:

  • We import kotlinx.coroutines.* for coroutines and java.net.URL for HTTP requests.
  • The fetchDataFromApi function asynchronously fetches data from an API URL.
  • In main, we define a list of API URLs to fetch data from concurrently.
  • Using async and awaitAll, we launch asynchronous jobs for each API URL to fetch data concurrently.
  • After all jobs finish fetching data, we process and print the responses.

This example showcases how concurrency can improve API request performance by fetching data from multiple endpoints simultaneously.

These examples demonstrate practical scenarios where concurrency and parallelism in Kotlin can significantly enhance the performance of various tasks, such as web scraping, image processing, and API requests.

Conclusion

Concurrency and parallelism are essential concepts in software development, and Kotlin provides robust mechanisms like threads and coroutines to achieve them effectively. By understanding these mechanisms and leveraging them in real-world scenarios, developers can build highly efficient and performant applications that execute multiple tasks simultaneously.