Jim H
Jim H
  • Home
  • Resume
  • Code Review-BestPractices
  • Java Best Practices
  • Kotlin Best Practices
  • Homophones in English
  • Personal
  • Blog

Concurrent Execution

Back to Contents


Concurrent execution in Kotlin is done through threads and coroutines.  A brief summary of threads is below; coroutines are handled in much more detail, as they are far more important in Kotlin programming.


Coroutines

Data integrity


Threads

Threads, as a concept, are well-used throughout the industry, from pthreads under Unix, to Java Thread objects. Depending on the implementation, a thread may have states like Running, Ready, Suspended, and Stopped.  Typically only one thread (or one thread per core) is Running; but from the programmer's point of view, many threads are running simultaneously.

To launch a thread in Kotlin, use the thread() function.

val myThread = thread(start = true) {

    //   code 

}

Coroutines

Back to Contents

Back to section top


A coroutine is an instance of a suspendible computation. Think of a coroutine as a very lightweight thread. Similar to a thread, but not bound to a thread: a coroutine may suspend its execution in one thread and resume in a different thread. 

Coroutine context and dispatchers are covered in the Kotlin documentation;  a brief summary follows here.


Coroutine Scope

Coroutines are created within a coroutine scope. The simplest, and first, scope is called runBlocking { }, which will often be run from main() or a test function. You can create a coroutine scope with coroutineScope { }; the main difference is that runBlocking will create a scope and run it, but coroutineScope will create a scope and suspend it.


launch

To create and start a coroutine, you can put it inside a launch block. 

val job = launch {

    println("running inside a coroutine")

}

A launch creates a Job object.


async 

Similar to launch, async creates a coroutine, but instead of a Job, async returns a Deferred<T>, where T is the type of object returned. This is analogous to a Future<T> in Java.

val deferred = async {

    6 * 7

}

println("The answer is ${deferred.await()}"    // 42

Here deferred has type Deferred<Int>; the value of the block is the value of its final expression.


Channels

To communicate between coroutines, create channels. A Channel<T> will transport objects of type T between coroutines; the sender uses Channel.send(T), and the receiver will use Channel.receive() to receive a T. Either send(T) or receive() can block the calling coroutine: the sender, if the channel is "full", and the receiver if the channel is empty. It's possible to create a channel for which send(T) will never suspend, either because its capacity is limited only by memory, or because items are purged from the channel when a new item arrives.


Dispatchers

There are some "standard" coroutine dispatchers:

  • Dispatchers.Default: as the name indicates, the default dispatcher, which dispatches coroutines into a thread pool (1 thread per core).
  • Dispatchers.Main: always dispatches onto the main thread (suitable for UI).
  • Dispatchers.IO: suitable for IO-bound coroutines.
  • Dispatchers.Unconfined: not confined to any specific thread. It executes the initial continuation of a coroutine in the current call-frame and lets the coroutine resume in whatever thread that is used by the corresponding suspending function, without mandating any specific threading policy.

Data Integrity

Latches and Mutexes and Locks, Oh My!

Back to Contents

Back to section top


When multiple threads or coroutines share the same objects, they can inadvertently stomp on each others' work. Kotlin has several data structures to avoid this trap. (As does Java.) These are provided in the package kotlin.concurrent.


Atomic Values

Kotlin provides AtomicInt, AtomicLong, and atomic array objects. These values are always updated atomically; that is, they are guaranteed not to change in unexpected ways when they are actively being updated. 

  • AtomicInt.addandGet(delta: Int) will take the current value, add delta, update the object, and return the sum. 
  • getAndAdd(delta: Int) method will return the previous value, and update the object. The method getAndSet(newValue: Int) will atomically replace the value, returning the old value. 
  • There are also methods incrementAndGet() and getAndIncrement(), which respectively act like the prefix ++ and postfix ++ operators. 
  • There are getAndDecrement() and decrementAndGet() variants. 
  • There is no need for subtractAndGet, since one can add a negative number.


java.util.Timer

The Java Timer class is a facility for threads to schedule execution of tasks. There are methods in kotlin.concurrent that use Timer objects.

  • schedule(): sets a task to be performed after some specified delay.
  • timer(): sets a task to be performed repeatedly, with a fixed delay between the end on one invocation and the start of the next one.l
  • fixedTimerTask(): sets a task to be performed repeatedly, with a fixed delay between the start of one invocation and the start of the next one.


java.util.concurrent

Don't forget about all of the Java concurrent collections classes.  Java classes are interoperable with Kotlin classes.

Stateless Execution

Back to Contents


As described in Best Practices In Java, when running concurrently in coroutines (or threads), the objects and methods that do the work should not carry state about the work being done, except in very simple circumstances. Rather, keep the context in a separate object. This way, worker methods can work multiple parallel processes, each with one context object.

Encapsulation

Back to Contents


[Prerequisite: Please read corresponding section in my Java BP article]


Encapsulation is one of the key concepts of Object Oriented Programming: an object has a single responsibility (the S in SOLID) that hides details of its implementation. In Best Practices In Java, we see several ways to further encapsulate within classes.  All of these are also present in Kotlin. But there is an important addition to Lambda expressions in Kotlin: a lambda that has a single parameter may omit that parameter, and use the implicitly defined "it" in its place.


fun getStrings(): List<String> { /* code */ }

//  ...

val myList = getStrings()

myList.forEach { println(it) }


Kotlin also supports local functions. That is, a function may be defined inside another function, and therefore be accessible only inside the enclosing function. A local function may access symbols defined in the enclosing function.

Type Aliases

A feature that is "missing" in Java, that we first encountered in C and C++

Back to Contents


Sometimes type names get really long–especially for generic types:

    val updateTimeMap: Map<String, List<Pair<File, Instant>>> = ...

Instead, Kotlin provides typealias to shorten such things.

    typealias UpdateTimeMap = Map<String, List<Pair<File, Instant>>>

    val updateTimeMap: UpdateTimeMap = ...

This is exactly analogous to C's typedef. It does not create a new type, but merely provides "syntactic sugar" to shorten the name.

Scope Functions

Back to Contents


Kotlin's standard library contains a few functions which provide a temporary scope (inside a lambda) to provide simpler access to an object. The functions are: let, run, with, apply, and also. There are subtle differences among them, mainly regarding how the object is accessed within the lambda, and what the result of the whole expression is. The scope functions are documented here.


Refer to the Context Object

Using run, with, or apply, refer to the context object as this (which might be implicit). For example:

val string = "hello cruel world"

string.run {

    println("The string's length is $length; or maybe ${this.length}.")

}

Both forms are identical; the code will print "The string's length is 17; or maybe 17."


Using let or also, refer to the context object as it.

val string = "goodbye cruel world"

string.let {

    println("This string's length is ${it.length}."

}

Now, the output is "This string's length is 19."


Expression Value

Using apply or also, the expression's value is the context object. Using let, run, or with, the value of the final expression of the lambda is the whole expression's value.

Serialization

Back to Contents


The Serialization section of the Java Best Practices article says, "The de facto standard for serialization is the Jackson library (com.fasterxml.jackson.*)." Since Kotlin classes are compatible with Java classes, that could have been the end off the discussion. However, Kotlin has its own serialization library, and that is discussed here. (Documentation) (KDoc) (GitHub)


The Documentation link above very helpfully tells how to set up your project to use kotlinx-serialization with Gradle; if you're a Maven fan, fall back on the good folks at Baeldung. The synopsis is:

1. Add the serialization plugin to the Kotlin compiler plugin:

<build>

    <plugins>

        <plugin>

            <groupId>org.jetbrains.kotlin</groupId>

            <artifactId>kotlin-maven-plugin</artifactId>

            <version>${kotlin.version}</version>

            <executions>

                <execution>

                    <id>compile</id>

                    <phase>compile</phase>

                    <goals>

                        <goal>compile</goal>

                    </goals>

                </execution>

            </executions>

            <configuration>

                <compilerPlugins>

                    <plugin>kotlinx-serialization</plugin>

                </compilerPlugins>

            </configuration>

            <dependencies>

                <dependency>

                    <groupId>org.jetbrains.kotlin</groupId>

                    <artifactId>kotlin-maven-serialization</artifactId>

                    <version>${kotlin.version}</version>

                </dependency>

            </dependencies>

        </plugin>

    </plugins>

</build>


2. Add the runtime dependency:

<dependency>

    <groupId>org.jetbrains.kotlinx</groupId>

    <artifactId>kotlinx-serialization-json</artifactId>

    <version>${serialization.version}</version>

</dependency>


2.5 Maven/Reload: make sure the compiler plugin gets loaded.


3. Decorate your data classes with the @Serializable annotation.

@Serializable

data class Person(val first: String, val last: String, val title: String) 


4. To serialize, call Json.encodeToString()

val person = Person("John", "Doe", "Sales Manager")

val json = Json.decodeToString(person)

println(json)

// output is: {"first": "John", "last": "Doe", "title": "Sales Manager"}


5. To deserialize: call Json.decodeFromString()

val json = """{"first": "Jane", "last": "Roe", "title": "Chief Marketing Officer"}"""

val person = Json.decodeFromString<Person>(json)

println(person)

// output is: Person(first=Jane,last=Roe,title=Chief Marketing Officer)

Notes on Serialization

  1. Only properties with backing fields are serialized.
  2. Properties to be serialized must be named in the primary constructor.
  3. To validate a property, use an init block.
  4. A property may be marked @Transient to leave it out of the serialization.
  5. To deserialize a transient property, it should have a default value. (Default values are not serialized by default; there is an experimental feature to change that.)
  6. A nullable property can have a default value of null, and therefore won't be serialized if it is null.
  7. Serializable classes may have other serializable class objects as properties. The resulting JSON will nest one JSON document inside the other.
  8. The name of a JSON field can be controlled with the @SerialName annotation on the corresponding property.
  9. Built-in types will serialize automagically. There is a facility for building custom (de)serializers, by implementing the interface KSeralizer. See the documentation on GitHub.
  10. Unknown keys in the JSON may be ignored by specifying a flag in a JsonBuilder declaration, as shown here:

val format = Json { ignoreUnknownKeys = true }
@Serializable

data class Outer(val a: Int, val b: Int)

// keys other than "a" and "b" are ignored

 

There is much more in the documentation than can be covered here. 

(Documentation) (KDoc) (GitHub)

First PagePreviousNextHome

Copyright © 2019-2025 Jim Hamilton - All Rights Reserved.

Powered by

This website uses cookies.

We use cookies to analyze website traffic and optimize your website experience. By accepting our use of cookies, your data will be aggregated with all other user data.

DeclineAccept