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.
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
}
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.
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.
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.
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.
There are some "standard" coroutine dispatchers:
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.
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.
java.util.concurrent
Don't forget about all of the Java concurrent collections classes. Java classes are interoperable with Kotlin classes.
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.
[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.
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.
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.
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)
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)