I understand that the race condition occurs when two threads access the same state, each one modifying it on its own. After this, the resulting value will be different than expected and may even be different on each run. Here an example of a critical section (in Java):
public class Counter {
protected long count = 0;
public void add(long value){
this.count = this.count + value;
}
}
If Pedro and Ana execute the add() function in a different thread, one adds 5 and the other adds 2. The result will be the value written by the last thread. Therefore, if Pedro's thread is executed last, the value will be 5. But if Ana's thread is executed last, the value will be 2. What really should have happened is that the two values were added and the final result out 7.
In Scala applying "functional programming" they tell us to avoid shared states and that we must use immutable structures such as ADTs. But to understand this solution I need an example where this concurrence is simulated, it can be that of a consignment in a savings account. Let's say that Luis and Maria deposit $10 and $5 respectively in the same account at the same time. Suppose that the account information is stored in a database, we are also working with futures in Scala and in addition to that, when making a deposit, the system immediately gives you the current balance. How does immutability and unshared states play into the user being shown the correct information under this scenario?
Here is the scala code that exemplifies the race condition:
import scala.concurrent.Future
import scala.util.{Failure, Success}
import scala.concurrent.ExecutionContext.Implicits.global
trait Cuenta
case class CuentaAhorros(numero:String, cantidad:BigDecimal, idUser:String) extends Cuenta
trait OperacionesCuenta {
def buscarCuenta(numeroCuenta:String):Future[CuentaAhorros] = Future{
// supongamos que fuimos a bd y nos trajo la cuenta
CuentaAhorros("021-123-456-11", 20, "1067905803")
}
def consignar(cuentaAhorros: CuentaAhorros, valor:BigDecimal):Future[Cuenta] = Future{
cuentaAhorros.copy(cantidad = cuentaAhorros.cantidad + valor)
}
def transaccion(numeroCuenta:String, valor:BigDecimal): Future[Cuenta] =
for {
cuentaObjetivo <- buscarCuenta(numeroCuenta)
cuentaActualizada <- consignar(cuentaObjetivo, valor)
}yield cuentaActualizada
}
object Operaciones extends App with OperacionesCuenta{
val transaccion1: Future[Cuenta] = transaccion("021-123-456-11", 20)
val transaccion2: Future[Cuenta] = transaccion("021-123-456-11", 40)
transaccion1 onComplete {
case Success(cuenta) => println("Cuenta en la trasacción 1 = " + cuenta)
case Failure(e) => println("A ocurrido un error: " + e.getMessage)
}
transaccion2 onComplete {
case Success(cuenta) => println("Cuenta en la trasacción 2 = " + cuenta)
case Failure(e) => println("A ocurrido un error: " + e.getMessage)
}
Thread.sleep(2000)
}
The result of the execution was the following:
Account in transaction 1: CuentaAhorros(021-123-456-11,40,1067905803)
Account in transaction 2: CuentaAhorros(021-123-456-11,60,1067905803)
Futures seem like a simple way to launch threads, but their execution terminates when the result ( promise ) is returned . From then on it will always use the calculated value, so any side effects it does will no longer be repeated.
For example:
The future
trans
only prints a single"Hola"
, although it returns the value1
as many times as required.There is no single way to design an application for concurrent execution, and it would be too broad a topic to cover here. Each concurrent application requires a different solution. This problem is probably best solved with the actor model (library
akka
).In our case, it is not possible to emulate a bank transaction with futures. We need an object that acts as a Bank that operates with our accounts. In production, this work would be performed by a database, web service, or other entity. In the example that I put, it is emulated with the object
Banco
that controls the status changes of the accounts. I put the code and then I explain it:I have used a technique called CAS (check and swap) . When the object
Banco
has to consolidate the balance, it first checks that the balance with which the process has been working is identical to the current one. Otherwise, it rejects the operation because it assumes that another process had modified the balance. The process that sees its operation rejected, restarts and starts again.We can ensure that this design for the transaction will be an "atomic operation" (at least one of the processes will finish in a fixed number of steps). If we execute the program several times, we will see that some random processes will have to restart their process, but in the end all of them will be executed and the final balance will always be the same. It is probably not the most optimized way to solve this problem.
I hope it has served to answer your question.