Instructions
This lesson is about the concept of concurrency in programming. Before you start coding, you need to understand what concurrency is and how it affects your programs.
Learning About Concurrency
Concurrency is the ability to run two or more tasks in overlapping time periods. A task isn’t necessarily related to computing. It can be anything that requires time to complete. For example, learning or eating are tasks. Take a look at the following diagram:
Note that the concurrent tasks don’t have to run at the same instant. You can take a lunch break while learning, or you can eat something while watching a video of this lesson or reading its text. In the latter case, the tasks are also parallel.
Running multiple tasks in parallel requires multiple amounts of computing power. Thus, there are hard limits on how many tasks can run in parallel. You can perform only several tasks at the same time. For example, you can eat and watch a single video at once. But you can’t watch ten videos simultaneously. It is physically impossible.
On the other hand, you can run more tasks concurrently without parallelism. For example, you can start as many different Kodeco courses as you want at the same time. You can watch one lesson from each course, one after another. You only have to remember what courses you are watching and where you stopped.
There is no physical limit on how many courses you can take concurrently. But, starting too many of them doesn’t make sense. Some of them will become obsolete before you finish them. But, that’s another story.
Similarly, your smartphone can only run a few tasks in parallel. The exact number depends on the number of CPU cores. In the case of smartphones offered at the time of preparing this lesson, that number usually varies from four to eight. If there are more tasks, they’ll go into the queue.
The maximum number of tasks that can run concurrently on a smartphone is much higher than can run in parallel. The tasks in the queue are waiting for their turn to run. If the queue becomes too long, the time to complete the tasks becomes unacceptable. It’s like downloading a dozen large files at the same time on a very slow internet connection.
Using Threads
Before learning about threads, you need to understand processes.
A process is an instance of a running program. The vast majority of Android apps run in a single process. If you tap the app icon again, the system brings the app to the foreground instead of starting a new instance.
On the other hand, if you open two terminal tabs in Android Studio, each of them runs in their own separate process of the shell. You can open them by pressing ⌥F12
and then ⌘T
on macOS or
Alt+F12
and then Ctrl+Shift+T
on Windows.
It’s possible to create a multi-process app in Android, but it’s a very advanced case and beyond the scope of this module. The process is an element of the operating system. All the programs running on a given operating system are backed by the same kind of structure. No matter which programming language or technology they were written in.
OK, now you know what a process is. It’s time to learn about threads.
In programming, the thread is a basic unit of execution. In other words, it’s a basic part executed concurrently within the process. Look at the following diagram:
The diagram shows a process with two threads. One of them has a higher priority than the other. This means that the operating system will give more execution time in comparison to the other thread.
Unlike a process, a thread is specific to the programming language and technology but not to the operating system on which the program runs. This module is about Android apps written in Kotlin. So, the lessons cover only threads in Kotlin compiled to the JVM target. The details of threads in other programming languages and technologies may differ.
Note that the Kotlin standard library provides a high-level API for working with threads in the kotlin.concurrent
package. But that API is mostly a wrapper around the Java standard library. Thus, it’s not available when compiling Kotlin to JavaScript or targets other than the
JVM.
Each thread in the JVM has its own stack. The stack is a memory area where the thread stores local variables and the history of the method calls. If you exceed the limit on the size of the stack, the program throws a StackOverflowError
. This may happen when you have infinite recursion in your code. In case of an uncaught exception, the thread terminates. It won’t execute any more code. By default, the uncaught exceptions cause the app process to terminate. Or, in other words, the app crashes.
If you have a mutable state in your program, you have to be careful when working with multiple threads. The mutable state is a variable that can change its value. If two threads try to change the same variable at the same time, the result may be unpredictable. Depending on the exact timing of the threads, either of the threads may win. The result may be different every time you run or debug the program. It’s called a race condition.
Take a look at the following code:
val courses = mutableListOf(
"Kotlin Fundamentals",
"iOS Networking",
"Flutter Basics"
)
println(courses)
courses.removeFirst()
You may think the program will remove the “Kotlin Fundamentals” course from the list. It may not be true if another thread adds a new course to the list between the println
and removeFirst
.
What’s more, the opposite is also possible. The JVM optimizes performance by reordering the instructions and caching values. Imagine the following code:
var count = 0
fun waitForNonZeroCount() { //called in thread #1
println("Count is $count")
while (count == 0) {
println("Waiting for count to be greater than 0...")
}
println("Count is greater than 0!")
}
fun incrementCount() { //called in thread #2
count++
}
The compiler may notice that the count
variable isn’t modified in the waitForNonZeroCount
, so the compiled code may not read the value of the count
variable in the loop but use the cached value. It may result in an infinite loop. Keep in mind that one of the threads may not see the changes made by the other threads.
Writing a correct multithreaded code isn’t easy. There are dedicated thread-safe data structures and synchronization mechanisms to help you with that.
Spawning a new thread is a costly operation. It requires the operating system to allocate a new stack and some other resources. Thus, it’s not a good idea to spawn a new thread for every task you want to run concurrently. To make a single network request, for example.
In real-world applications, there are usually pools of threads and queues. Tasks like network requests are put into the queue. Then the threads from the pool take the tasks from the queue.
Main Thread
Every Android app has a main thread. It’s the thread where all the UI operations are executed. For example, the onCreate
method of the Activity
class is called on the main thread. All the operations that change the UI have to run on the main thread. If you try to change the UI from another thread, the system throws an exception.
Why is there such a restriction? Do you remember that a mutable state is hard to manage in a multithreaded environment? The ability to change the UI from any thread would make the underlying code much more complex. What’s more, the proper synchronization of the UI operations would hinder performance. However, the UI needs to be responsive. It’s much easier to restrict the UI operations to a single thread. That’s a common pattern in Graphical User Interface (GUI) programming, not only in Android.
It’s important to keep the main thread responsive. If you block the main thread for too long, users will notice the app isn’t responding. They may quickly close it and never open it again. All long-running operations, such as network requests or heavy computations, should happen on background threads. On the most recent Android versions, performing the network requests on the main thread is impossible. The system throws a NetworkOnMainThreadException
in such a case.
OK, now you know what threads are and how they affect the program’s execution. It’s time to learn how to work with threads in Kotlin.