What atomicity means

In the context of software execution, an operation is atomic when it is single and indivisible. When talking about shared state, we are often talking about reading or writing to a single variable from many threads.

A challenge is raised because modifying the state of a variable is usually not atomic, since it usually consists of multiple steps like reading, modifying, and storing the updated value. During the execution of concurrent applications it's possible that a block of code that modifies a shared state does it by overlapping changes from other threads—for example, one thread can read the current value while another is modifying it, but before it being written. This situation will mean that one or more modifications to that shared state will be lost as it is overwritten.

Let's consider this simple function as an example:

private var counter = 0

fun increment() {
counter ++
}

When executed sequentially, we are able to call increment() as many times as we want without having to worry about the value of counter. The value of counter will always correspond to the amount of times that increment() was called.

But when we add concurrency to the equation, a whole lot of things change under the hood. Let's start with this basic asynchronous function from Chapter 1, Hello, Concurrent World!:

var counter = 0

fun asyncIncrement(by: Int) = async(CommonPool) {
for (i in 0 until by) {
counter++
}
}

It's increasing counter as many times as requested, using CommonPool as its CoroutineContext.  We can call it from a main function like this – assuming that it runs in a device with more than one processing unit:

fun main(args: Array<String>) = runBlocking {
val workerA = asyncIncrement(2000)
val workerB = asyncIncrement(100)

workerA.await()
workerB.await()

print("counter [$counter]")
}

You will notice that after execution, the value of counter will often be lower than 2100:

This happens because the code counter++ is not atomic. This single line of code can be broken into three operations: read, modify, and write, and because of how threads work, the write changes in one thread may not be visible for other threads as they read or modify the value, meaning that many threads can potentially increase counter to the same value.

What this means in practice is that many cycles of the for inside asyncIncrement could affect the value of the counter only once. Let's review the diagram explanation from Chapter 1, Hello, Concurrent World!:

In this diagram, we have a representation of a scenario in which two threads read the same value in a way in which the read and modify overlap, so one of the increments is lost.

To make any block of code atomic, we need to guarantee that all the memory accesses happening within the block cannot be executed concurrently. This can be accomplished in many different ways, and the best way to do it largely depends on the specifics of the situation.

Notice that in the previous example, to effectively reproduce the error you may have to run it multiple times. That's part of the problem with issues related to concurrency: they occur only under certain circumstances that may be difficult to identify or reproduce.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.154.64