Resumo : neste tutorial, você aprenderá como usar a lock
instrução C# para evitar condições de corrida e garantir a segurança do thread quando vários threads acessam recursos compartilhados.
Condições da corrida
Vamos começar com um programa simples:
int counter = 0;
void Increase()
{
for (int i = 0; i < 1000000; i++)
{
counter++;
}
Console.WriteLine("The counter is " + counter);
}
Task.Run(() => Increase());
Task.Run(() => Increase());
Console.ReadLine();
Linguagem de código: C# ( cs )
Como funciona.
Primeiro, defina uma variável counter
e inicialize-a com zero.
Segundo, defina o Increase()
método que adiciona um à counter
variável 1 milhão de vezes e exibe o valor final do contador no console.
Terceiro, crie duas tarefas que sejam executadas em threads separados . Cada tarefa executa o Increase()
método para aumentar a counter
variável.
Como ambas as tarefas modificam a counter
variável simultaneamente, há risco de condições de corrida e problemas de sincronização.
Quando o programa aumenta a variável counter
, ele realiza três etapas:
- Leia o valor da
counter
variável. - Aumente em um.
- Escreva o novo valor de volta na
counter
variável.
Em outras palavras, o aumento da counter
variável não é uma operação atômica.
Portanto, uma tarefa pode ler o valor de counter
antes que a outra tarefa tenha concluído a atualização, resultando em um valor incorreto ou em uma atualização perdida.
Aqui está a saída do programa:
The counter is 1113815
The counter is 1515277
Linguagem de código: C# ( cs )
Observe que você poderá ver counter
valores diferentes ao executar o programa várias vezes devido às condições de corrida entre duas tarefas (ou threads).
É por isso que a lock
declaração vem em socorro.
Introdução à instrução de bloqueio C#
A lock
instrução evita condições de corrida e garante a segurança do thread quando vários threads acessam a mesma variável compartilhada.
Para usar a lock
instrução você cria um novo objeto que serve como bloqueio, também conhecido como mutex. O mutex significa exclusão mútua.
lock(lockObject)
{
// access the shared resources
}
Linguagem de código: C# ( cs )
Quando um thread entra em um lock
bloco, ele tentará adquirir o bloqueio no arquivo lockObject
.
Se o bloqueio já foi adquirido por outro thread, o thread atual será bloqueado até que o bloqueio seja liberado.
Depois que o bloqueio é liberado, o thread atual pode adquiri-lo e executar o código no lock
bloco que frequentemente lê ou grava os recursos compartilhados.
O programa a seguir demonstra como usar uma lock
instrução para evitar uma condição de corrida entre dois threads:
int counter = 0;
object lockCounter = new();
void Increase()
{
for (int i = 0; i < 1000000; i++)
{
lock (lockCounter)
{
counter++;
}
}
Console.WriteLine("The counter is " + counter);
}
Task.Run(() => Increase());
Task.Run(() => Increase());
Console.ReadLine();
Linguagem de código: C# ( cs )
Este programa é semelhante ao programa anterior, mas usa uma instrução lock para sincronizar o acesso à counter
variável.
A lock
instrução garante que ambas as tarefas acessem a counter
variável de forma mutuamente exclusiva.
Isso significa que o valor final do contador é previsível e sempre igual à soma dos incrementos realizados por ambas as tarefas, que é 2,000,000
.
Aqui está a saída do programa:
The counter is 1992352
The counter is 2000000
Linguagem de código: C# ( cs )
Observe que o valor final do contador é sempre 2,000,000
. Entretanto, o valor imediato ( 1,992,352
) pode variar porque o programa está sendo executado simultaneamente, com dois threads correndo para incrementar a variável do contador.
Práticas recomendadas de bloqueio C#
A seguir estão algumas práticas recomendadas ao usar a lock
instrução C#:
- Mantenha o
lock
bloco o menor possível para minimizar o tempo que outros threads terão que esperar pelo bloqueio. Se umlock
bloco demorar muito para ser executado, poderá causar contenção entre threads e reduzir o desempenho do aplicativo. - Evite bloqueios aninhados porque eles podem causar um conflito . Os deadlocks ocorrem quando vários threads tentam adquirir bloqueios em uma ordem diferente.
- Use
try...finally
block para liberar o bloqueio corretamente quando ocorrer uma exceção nolock
bloco. Além disso, garante que outros threads não sejam bloqueados indefinidamente. - Considere usar mecanismos de sincronização alternativos como
SemaphoreSlim
eReaderWriterLockSlim
porque eles podem fornecer melhor desempenho e controle mais refinado sobre a simultaneidade.
Resumo
- Use a
lock
instrução C# para evitar condições de corrida e problemas de sincronização quando vários threads acessam os mesmos recursos compartilhados.