Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Parallel Programming

Let's break down what happens when the following code is run in parallel:

var x = 3
x = x + 1
x = x * 2
println(x)

Running in Parallel

When the code runs sequentially in a single thread, it executes predictably. However, if the operations are run in parallel, the behavior can become unpredictable due to race conditions. Let's examine this in the context of parallel execution.

Potential Parallel Execution:

Let's assume we try to run the increment and multiplication steps in parallel threads:

import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.concurrent.duration._

var x = 3

val increment = Future {
  x = x + 1
}

val multiply = Future {
  x = x * 2
}

Await.result(increment, Duration.Inf)
Await.result(multiply, Duration.Inf)
println(x)

Potential Issues in Parallel Execution:

  1. Race Condition:
  • A race condition occurs when two or more threads access shared data and try to change it simultaneously. The result depends on the order in which the threads execute.
  1. Indeterminate Results:
  • The final value of x depends on the timing of the increment and multiply operations.
  • If increment runs first and completes before multiply starts, x will be incremented to 4 first, then multiplied by 2 to get 8.
  • If multiply runs first and completes before increment starts, x will be multiplied by 2 to get 6 first, then incremented by 1 to get 7.
  • If both operations interfere with each other, intermediate states can result in completely unpredictable values.

Examples of Possible Outcomes:

  • Outcome 1:

    • increment reads x as 3.
    • multiply reads x as 3.
    • increment writes x as 4.
    • multiply writes x as 6.
    • Final value of x is 6.
  • Outcome 2:

    • multiply reads x as 3.
    • increment reads x as 3.
    • multiply writes x as 6.
    • increment writes x as 7.
    • Final value of x is 7.
  • Outcome 3:

    • increment completes entirely first, x becomes 4.
    • multiply completes next, x becomes 8.
    • Final value of x is 8.

Solutions to Ensure Correctness:

To ensure that the operations on x are performed correctly and predictably, you can use synchronization mechanisms:

  1. Synchronized Block:
  • Ensure that only one thread can modify x at a time.
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.concurrent.duration._

var x = 3
val lock = new AnyRef

val increment = Future {
  lock.synchronized {
    x = x + 1
  }
}

val multiply = Future {
  lock.synchronized {
    x = x * 2
  }
}

Await.result(increment, Duration.Inf)
Await.result(multiply, Duration.Inf)
println(x)
  1. Atomic Variables:
  • Use atomic variables to ensure atomicity of operations.
import java.util.concurrent.atomic.AtomicInteger
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.concurrent.duration._

val x = new AtomicInteger(3)

val increment = Future {
  x.incrementAndGet()
}

val multiply = Future {
  x.updateAndGet(n => n * 2)
}

Await.result(increment, Duration.Inf)
Await.result(multiply, Duration.Inf)
println(x.get())