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:
- 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.
- Indeterminate Results:
- The final value of
xdepends on the timing of theincrementandmultiplyoperations. - If
incrementruns first and completes beforemultiplystarts,xwill be incremented to 4 first, then multiplied by 2 to get 8. - If
multiplyruns first and completes beforeincrementstarts,xwill 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:
incrementreadsxas 3.multiplyreadsxas 3.incrementwritesxas 4.multiplywritesxas 6.- Final value of
xis 6.
-
Outcome 2:
multiplyreadsxas 3.incrementreadsxas 3.multiplywritesxas 6.incrementwritesxas 7.- Final value of
xis 7.
-
Outcome 3:
incrementcompletes entirely first,xbecomes 4.multiplycompletes next,xbecomes 8.- Final value of
xis 8.
Solutions to Ensure Correctness:
To ensure that the operations on x are performed correctly and predictably, you can use synchronization mechanisms:
- Synchronized Block:
- Ensure that only one thread can modify
xat 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)
- 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())