O que é Race Condition?
Race condition é um problema comum que acontece em programação concorrente, onde duas threads modificam o mesmo recurso simultaneamente, podendo gerar resultados inesperados.
Um exemplo prático: Conta Bancária
Imagine um banco digital, que ao realizar débito em conta, verifica se você possui saldo suficiente. Veja o modelo abaixo:
Para garantir que está tudo certo, escrevemos um teste unitário:
Executamos o teste e verificamos que o Débito em conta está funcionando corretamente.
Agora que sabemos como o débito em conta funciona, vamos criar um cenário hipotético onde você possui R$ 2000,00 em conta e tenta realizar a compra de um celular (R$ 1500) e uma geladeira (R$ 1700) exatamente ao mesmo tempo.
O resultado esperado é que uma das compras seja negada por saldo insuficiente. E para simular isso, criamos o seguinte programa:
Ao executar temos o seguinte resultado:
Sim, não aconteceu conforme o esperado. O programa permitiu a compra, mesmo não tendo saldo suficiente em conta.
Explicando a causa
Mas por que raios isso aconteceu, sendo que nosso teste unitário garantiu que isso não ocorreria?
Antes, vamos entender como o programa executou as compras:
- Saldo na conta: R$ 2000,00
- Inicia débito em conta de R$ 1500,00 do Celular
- Inicia débito em conta de R$ 1700,00 da Geladeira
- Saldo em conta é suficiente (Celular)? Sim, saldo de R$ 2000,00
- Saldo em conta é suficiente (Geladeira)? Sim, saldo de R$ 2000,00
- Saldo suficiente, então realiza o débito (Celular)
- Saldo atual R$ 500,00
- Saldo suficiente, então realiza o débito (Geladeira)
- Saldo atual R$ -1200,00
Resumindo, as duas compras olharam para o mesmo saldo de R$ 2000,00, porque acessaram o mesmo recurso simultaneamente, antes de ocorrer o débito da outra compra. E por isso, passaram com sucesso pela etapa de validação de saldo.
Como resolver o problema?
Permitir que apenas uma compra debite da conta bancária por vez.
Se duas compras são feitas simultaneamente, uma deve iniciar o processo de débito e a outra deve esperar ela terminar para depois começar.
Para atingir esse objetivo, nós podemos usar um recurso chamado de “lock” que tranca o objeto enquanto ele está sendo usado por alguém. E quando esse alguém terminar de usar, libera o acesso para quem estava esperando.
Então, a forma correta de debitar da conta bancária usando “lock” seria assim:
Ao executar o programa novamente temos o seguinte resultado:
Conclusão
Ao usar programação concorrente devemos estar sempre atentos a esses tipos de cenários e evitar sérios problemas, como o caso da conta bancária.
Ao criar um novo recurso, pergunte-se “se mais de uma thread acessarem esse recurso simultaneamente, terei problemas?”
Se a resposta for sim, avalie se é necessário aplicar algum tipo de lock.