C# lock
Nas aulas de Task e Threads, você pôs o computador para fazer várias coisas ao mesmo tempo. Isso é ótimo enquanto cada tarefa mexe só nas suas coisas. O problema começa quando duas tarefas mexem na mesma variável no mesmo instante: o resultado sai errado, e cada vez errado de um jeito diferente. Nesta aula você vai ver esse erro acontecer de verdade e consertá-lo com a instrução lock ("trancar") e com o tipo Lock, novo no .NET 9.
- O que é uma condição de corrida e por que
contador++não é seguro com várias threads. - A instrução
lock (trava) { … }e o que ela garante. - O tipo
System.Threading.LockC# 13, que é o jeito recomendado no .NET 9 e no .NET 10. - Proteger uma classe inteira (a conta do banco e o estoque da lojinha) com um campo
private readonly Lock _trava = new();. EnterScope,TryEntereInterlocked, para casos especiais.- O impasse (deadlock) e os erros
CS0185,CS1996,CS4007e o avisoCS9216.
Todas as saídas desta aula são reais: foram feitas com o .NET 10 (dotnet --version = 10.0.112) na máquina fictícia da Ana, um notebook com 8 núcleos, na pasta ~/cs/lock, com dotnet run arquivo.cs (veja Rodar com dotnet run).
A ideia: a chave do banheiro do posto
Num posto de estrada, o banheiro tem uma chave só, pendurada no balcão. Quem quer entrar pega a chave; quem chega depois espera no balcão até a chave voltar. Ninguém entra enquanto outra pessoa está lá dentro.
O lock é essa chave. Um pedaço do código vira o "banheiro": só uma thread por vez pode estar dentro dele. As outras ficam esperando, em fila, até a primeira sair. Esse pedaço protegido tem um nome técnico: seção crítica.
| No posto | No C# |
|---|---|
| O banheiro | O bloco { … } do lock (a seção crítica) |
| A chave pendurada no balcão | O objeto da trava: Lock _trava = new(); |
| Pegar a chave | Entrar no lock (_trava) |
| Esperar no balcão | A thread fica parada até a trava ficar livre |
| Devolver a chave | Sair do bloco (sempre, mesmo se der erro lá dentro) |
1. O problema: quatro tarefas, um contador
A Ana quer contar até 4 milhões, dividindo o trabalho entre 4 tarefas: cada uma soma 1 ao contador um milhão de vezes. A conta é fácil: 4 × 1.000.000 = 4.000.000.
int contador = 0;
var tarefas = new Task[4];
for (int i = 0; i < tarefas.Length; i++)
{
tarefas[i] = Task.Run(() =>
{
for (int j = 0; j < 1_000_000; j++)
{
contador++;
}
});
}
await Task.WhenAll(tarefas);
Console.WriteLine($"Esperado: {4 * 1_000_000}");
Console.WriteLine($"Total: {contador}");
- int contador = 0;
- A variável que as quatro tarefas vão dividir. Como a lambda
() => { … }usacontador, todas as tarefas mexem na mesma variável, não numa cópia (veja Lambdas). - Task.Run(() => { … })
- "Rode isto numa tarefa": o .NET pega uma thread livre e executa a lambda nela, ao mesmo tempo que as outras (veja Task).
- contador++;
- Soma 1. Parece uma coisa só, mas não é (veja logo abaixo).
- await Task.WhenAll(tarefas);
- Espera as quatro tarefas terminarem antes de mostrar o total.
A Ana rodou três vezes, sem mudar nada:
$ dotnet run contador.cs Esperado: 4000000 Total: 2843643 $ dotnet run contador.cs Esperado: 4000000 Total: 3213877 $ dotnet run contador.cs Esperado: 4000000 Total: 2927090
O total está errado, e cada vez de um jeito. Sumiram mais de um milhão de somas. Não apareceu nenhum erro nem nenhum aviso: o compilador não tem como saber que quatro threads vão mexer na mesma variável. Esse tipo de erro se chama condição de corrida (em inglês, race condition): o resultado depende de quem "chega primeiro", e isso muda a cada execução.
Por que contador++ perde somas
Para o processador, contador++ são três passos: ler o valor da memória, somar 1, gravar o valor de volta. Veja o que acontece quando duas threads fazem isso no mesmo instante, com o contador valendo 10:
| Momento | Thread A | Thread B | contador na memória |
|---|---|---|---|
| 1 | lê: 10 | 10 | |
| 2 | lê: 10 | 10 | |
| 3 | soma: 11 | soma: 11 | 10 |
| 4 | grava 11 | 11 | |
| 5 | grava 11 | 11 (devia ser 12) |
As duas somaram 1, mas o contador só andou 1: a soma da thread A foi apagada pela gravação da thread B. Com 8 núcleos trabalhando, isso acontece milhões de vezes.
2. A instrução lock
O conserto é deixar o contador++ dentro de um lock. Assim, só uma tarefa por vez faz os três passos:
int contador = 0;
object trava = new();
var tarefas = new Task[4];
for (int i = 0; i < tarefas.Length; i++)
{
tarefas[i] = Task.Run(() =>
{
for (int j = 0; j < 1_000_000; j++)
{
lock (trava)
{
contador++;
}
}
});
}
await Task.WhenAll(tarefas);
Console.WriteLine($"Esperado: {4 * 1_000_000}");
Console.WriteLine($"Total: {contador}");
$ dotnet run contador2.cs Esperado: 4000000 Total: 4000000 $ dotnet run contador2.cs Esperado: 4000000 Total: 4000000
- object trava = new();
- A "chave do banheiro": um objeto qualquer, criado só para servir de trava. Ele não guarda nada; o que importa é que todas as tarefas usem o mesmo objeto. É a forma antiga, que funciona em qualquer versão do .NET (a forma nova está na seção 3).
- lock (trava)
- "Tranque com esta trava". Se a trava está livre, a thread a pega e entra no bloco. Se outra thread está com ela, esta espera ali, parada, até a trava ser devolvida.
- { contador++; }
- A seção crítica: o que só uma thread por vez pode fazer. Ponha aqui tudo o que lê e muda os dados divididos, e nada além disso.
- a chave do bloco
} - Ao sair do bloco, a trava é devolvida e a próxima thread da fila entra. Isso acontece sempre: no fim normal, num
returnou numa exceção.
A trava só funciona se for a mesma para todos. Se cada tarefa criasse a sua (lock (new object())), seria como um banheiro com uma chave para cada pessoa: todo mundo entra junto, e o total volta a sair errado.
O lock devolve a trava mesmo quando dá erro
Uma exceção lá dentro não deixa a trava presa. A propriedade IsHeldByCurrentThread ("está segura pela thread atual?") mostra isso:
Lock trava = new();
try
{
lock (trava)
{
Console.WriteLine($"Dentro do lock: {trava.IsHeldByCurrentThread}");
throw new InvalidOperationException("deu ruim");
}
}
catch (InvalidOperationException e)
{
Console.WriteLine($"Pegou: {e.Message}");
}
Console.WriteLine($"Depois do erro: {trava.IsHeldByCurrentThread}");
$ dotnet run excecao.cs Dentro do lock: True Pegou: deu ruim Depois do erro: False
Por baixo, o compilador escreve o lock como um try/finally (veja finally): pega a trava, roda o bloco no try e devolve a trava no finally.
Missão: o contador abaixo divide o trabalho entre 4 tarefas, mas o contador++ está desprotegido. Ponha o contador++ dentro de um lock (trava) { … }, usando a trava que já está criada, e rode. O total tem de ser 400000.
Atenção: o editor do guia roda o .NET dentro do navegador com um núcleo só (lá, Environment.ProcessorCount vale 1): as tarefas rodam uma depois da outra, e o total sai certo mesmo sem o lock. É mais um exemplo de "rodou certo não prova nada". Por isso a missão confere também o seu código. No terminal, com dotnet run, o erro aparece.
int contador = 0;
Lock trava = new();
var tarefas = new Task[4];
for (int i = 0; i < tarefas.Length; i++)
{
tarefas[i] = Task.Run(() =>
{
for (int j = 0; j < 100_000; j++)
{
contador++;
}
});
}
await Task.WhenAll(tarefas);
Console.WriteLine($"Total: {contador}");
int contador = 0;
Lock trava = new();
var tarefas = new Task[4];
for (int i = 0; i < tarefas.Length; i++)
{
tarefas[i] = Task.Run(() =>
{
for (int j = 0; j < 100_000; j++)
{
lock (trava)
{
contador++;
}
}
});
}
await Task.WhenAll(tarefas);
Console.WriteLine($"Total: {contador}");
3. O tipo Lock C# 13
Até o .NET 8, a trava era sempre um object qualquer. O .NET 9 trouxe um tipo feito só para isso: System.Threading.Lock. Ele é mais rápido e deixa claro, só de ler, para que o campo serve. No .NET 10, é o jeito recomendado. Não precisa de using: o System.Threading já vem importado nos programas de console.
Basta trocar o tipo da trava; o lock continua igual:
int contador = 0; Lock trava = new(); «… o resto, igual …»
$ dotnet run contador2.cs Esperado: 4000000 Total: 4000000
O compilador do C# 13 em diante reconhece o tipo Lock no lock (…) e usa os métodos próprios dele. Com um object, ele usa o mecanismo antigo, o Monitor (veja a tabela "formas diferentes", abaixo).
Uma classe que se protege sozinha
Na vida real, a trava não fica solta no programa: ela fica dentro da classe que guarda os dados divididos, como um campo private readonly. Assim, quem usa a classe nem precisa saber que existe uma trava.
O caso: a conta conjunta da Ana e do Beto tem R$ 100,00. Os dois, cada um num caixa eletrônico, tentam sacar R$ 80,00 no mesmo instante. Primeiro, a versão sem trava (o Thread.Sleep(10) faz o caixa "pensar" 10 milissegundos entre conferir o saldo e descontar, como acontece num sistema de verdade, que consulta o banco de dados):
var conta = new Conta(100m);
var ana = new Thread(() => conta.Sacar(80m, "Ana"));
var beto = new Thread(() => conta.Sacar(80m, "Beto"));
ana.Start();
beto.Start();
ana.Join();
beto.Join();
Console.WriteLine($"Saldo final: {conta.Saldo:C}");
class Conta(decimal saldoInicial)
{
public decimal Saldo { get; private set; } = saldoInicial;
public void Sacar(decimal valor, string quem)
{
if (Saldo >= valor)
{
Thread.Sleep(10); // o caixa eletrônico "pensa" um pouco
Saldo -= valor;
Console.WriteLine($"{quem} sacou {valor:C}");
}
else
{
Console.WriteLine($"{quem}: saldo insuficiente");
}
}
}
$ dotnet run conta.cs Beto sacou R$ 80,00 Ana sacou R$ 80,00 Saldo final: -R$ 60,00 $ dotnet run conta.cs Ana sacou R$ 80,00 Beto sacou R$ 80,00 Saldo final: -R$ 60,00
Os dois conferiram o saldo (R$ 100,00, dá para sacar), os dois sacaram, e a conta ficou negativa. É o padrão "confere e depois faz" (em inglês, check-then-act): entre a conferência e a ação, outra thread mudou o mundo. Na primeira tentativa da Ana, o saldo final saiu até R$ 20,00, porque as duas threads leram R$ 100,00 e gravaram R$ 20,00 por cima uma da outra, o mesmo problema do contador. Cada execução pode errar de um jeito.
Agora com a trava. A conferência e o desconto ficam no mesmo lock:
class Conta(decimal saldoInicial)
{
private readonly Lock _trava = new();
public decimal Saldo { get; private set; } = saldoInicial;
public void Sacar(decimal valor, string quem)
{
lock (_trava)
{
if (Saldo >= valor)
{
Thread.Sleep(10); // o caixa eletrônico "pensa" um pouco
Saldo -= valor;
Console.WriteLine($"{quem} sacou {valor:C}");
}
else
{
Console.WriteLine($"{quem}: saldo insuficiente");
}
}
}
}
$ dotnet run conta2.cs Ana sacou R$ 80,00 Beto: saldo insuficiente Saldo final: R$ 20,00
- private
- Só a própria classe enxerga a trava. Ninguém de fora consegue trancá-la por engano (nem esquecer de trancar).
- readonly
- "Só leitura": a trava nunca é trocada por outra. Se alguém fizesse
_trava = new();no meio do programa, threads diferentes passariam a usar chaves diferentes. - Lock _trava = new();
- Uma trava para cada conta. Contas diferentes não esperam umas pelas outras: o Beto sacando da conta dele não atrapalha a Ana sacando da dela.
- lock (_trava) { if … Saldo -= valor; }
- A conferência e o desconto viraram uma coisa só, que ninguém interrompe. Quem chega depois espera e encontra o saldo já atualizado.
Quem sacou primeiro (Ana ou Beto) pode mudar de uma execução para outra: isso é normal com threads. O que o lock garante é que só um dos dois consegue, e que o saldo final é sempre R$ 20,00.
lock (por exemplo, no get da propriedade). Para um int sozinho a leitura é segura, mas para um decimal (16 bytes) a outra thread pode ler um valor "pela metade". A regra mais simples: todo acesso aos dados divididos passa pela trava.
4. EnterScope e TryEnter
O lock (…) resolve quase tudo. Para os casos especiais, o tipo Lock tem métodos próprios:
Lock trava = new();
using (trava.EnterScope())
{
Console.WriteLine($"Dentro: {trava.IsHeldByCurrentThread}");
}
Console.WriteLine($"Fora: {trava.IsHeldByCurrentThread}");
if (trava.TryEnter())
{
try
{
Console.WriteLine("Consegui a trava com TryEnter");
}
finally
{
trava.Exit();
}
}
$ dotnet run escopo.cs Dentro: True Fora: False Consegui a trava com TryEnter
- trava.EnterScope()
- "Entre num escopo": pega a trava e devolve um objeto que, no fim do
using, a solta (veja using). É exatamente o que olock (trava)faz por baixo quando a trava é umLock. - trava.TryEnter()
- "Tente entrar": se a trava está livre, pega e devolve
true; se está ocupada, devolvefalsena hora, sem esperar. Serve para "se o banheiro estiver ocupado, vou fazer outra coisa". Existe tambémTryEnter(TimeSpan.FromSeconds(1)), que espera no máximo esse tempo. - try { … } finally { trava.Exit(); }
- Com
TryEnter, devolver a trava é com você:Exit()("sair") nofinally, para devolver mesmo se der erro.
5. Para uma soma só: Interlocked
Quando o que você precisa proteger é uma única operação num número (somar 1, subtrair, trocar o valor), existe um atalho mais leve que o lock: a classe Interlocked ("travado entre si"). Ela pede ao processador que faça os três passos (ler, somar, gravar) como um só, que ninguém interrompe. Uma operação assim se chama atômica (do grego átomo, "que não se divide").
int contador = 0;
var tarefas = new Task[4];
for (int i = 0; i < tarefas.Length; i++)
{
tarefas[i] = Task.Run(() =>
{
for (int j = 0; j < 1_000_000; j++)
{
Interlocked.Increment(ref contador);
}
});
}
await Task.WhenAll(tarefas);
Console.WriteLine($"Total: {contador}");
$ dotnet run inter.cs Total: 4000000
- Interlocked.Increment(ref contador)
- Soma 1 de forma atômica. O
refentrega a própria variável, não uma cópia do valor (veja ref, out e in). - Outros métodos
Interlocked.Decrement(ref x)(subtrai 1),Interlocked.Add(ref x, 5)(soma 5),Interlocked.Exchange(ref x, 0)(troca o valor e devolve o antigo).
O Interlocked só serve para uma variável e uma operação. O saque da conta (conferir o saldo e descontar) são duas coisas que precisam acontecer juntas: aí, só o lock resolve.
Missão: o site da lojinha conta as visitas com 3 tarefas. Troque o visitas++ por Interlocked.Increment(ref visitas); e rode. A saída deve ser Visitas: 300.
int visitas = 0;
var tarefas = new Task[3];
for (int i = 0; i < tarefas.Length; i++)
{
tarefas[i] = Task.Run(() =>
{
for (int j = 0; j < 100; j++)
{
visitas++;
}
});
}
await Task.WhenAll(tarefas);
Console.WriteLine($"Visitas: {visitas}");
int visitas = 0;
var tarefas = new Task[3];
for (int i = 0; i < tarefas.Length; i++)
{
tarefas[i] = Task.Run(() =>
{
for (int j = 0; j < 100; j++)
{
Interlocked.Increment(ref visitas);
}
});
}
await Task.WhenAll(tarefas);
Console.WriteLine($"Visitas: {visitas}");
Formas diferentes: qual usar
| Forma | Quando usar |
|---|---|
private readonly Lock _trava = new(); + lock (_trava) | O padrão no .NET 9 e no .NET 10. Use sempre que puder. |
private readonly object _trava = new(); + lock (_trava) | Código que precisa compilar no .NET 8 ou anterior. Você vai ver essa forma em quase todo código antigo e nos cursos; ela funciona igual, só um pouco mais devagar. |
Monitor.Enter(obj) / Monitor.Exit(obj) | É o que o lock (objeto) vira por baixo. Raramente você escreve isso à mão. |
Interlocked.Increment(ref x) | Uma única operação num número: contadores, estatísticas. |
ConcurrentDictionary, ConcurrentQueue… | Coleções que já se protegem sozinhas (pasta System.Collections.Concurrent). Melhor que pôr lock em volta de cada uso de um Dictionary. |
SemaphoreSlim + await WaitAsync() | Quando a seção crítica precisa de await, que o lock não permite (veja "Quando dá errado"). |
lock (this), lock (typeof(Conta)) ou lock ("conta"). Compila, mas é perigoso: esses objetos são visíveis para outras partes do programa (o mesmo texto "conta" é o mesmo objeto no programa inteiro), e qualquer uma delas pode trancar a mesma trava sem você saber, causando esperas e impasses. Use sempre um campo private readonly criado só para isso.
Quando dá errado
A Ana e o Beto precisam da caneta e do caderno para escrever. A Ana pega a caneta primeiro; o Beto, o caderno primeiro:
Lock caneta = new();
Lock caderno = new();
var ana = new Thread(() =>
{
lock (caneta)
{
Console.WriteLine("Ana pegou a caneta");
Thread.Sleep(100);
Console.WriteLine("Ana espera o caderno...");
lock (caderno)
{
Console.WriteLine("Ana escreveu");
}
}
});
var beto = new Thread(() =>
{
lock (caderno)
{
Console.WriteLine("Beto pegou o caderno");
Thread.Sleep(100);
Console.WriteLine("Beto espera a caneta...");
lock (caneta)
{
Console.WriteLine("Beto escreveu");
}
}
});
ana.Start();
beto.Start();
ana.Join();
beto.Join();
Console.WriteLine("Fim");
$ timeout 10 dotnet run impasse.cs; echo "código de saída: $?" Ana pegou a caneta Beto pegou o caderno Ana espera o caderno... Beto espera a caneta... código de saída: 124
O que aconteceu: a Ana segura a caneta e espera o caderno; o Beto segura o caderno e espera a caneta. Nenhum dos dois solta o que tem, e os dois esperam para sempre. O programa não dá erro nem termina: ele simplesmente para. Aqui, o timeout 10 matou o programa depois de 10 segundos (o código 124 é do timeout); sem ele, a Ana teria de apertar Ctrl+C.
Como resolver: quando precisar de duas travas, pegue-as sempre na mesma ordem em todo o programa (primeiro a caneta, depois o caderno, para todo mundo). Melhor ainda: evite segurar uma trava enquanto pega outra, e deixe a seção crítica o mais curta possível.
error CS0185: "int" não é um tipo de referência como necessário pela instrução lock
int trava = 0;
lock (trava)
{
Console.WriteLine("dentro");
}
$ dotnet run erro1.cs /home/ana/cs/lock/erro1.cs(2,7): error CS0185: "int" não é um tipo de referência como necessário pela instrução lock Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: o lock precisa de um objeto de um tipo de referência (uma classe), porque a trava mora no próprio objeto. Um int é um tipo de valor: cada uso seria uma cópia, e cada thread teria a "sua" trava (veja Valor × Referência). Em inglês: 'int' is not a reference type as required by the lock statement.
Como resolver: crie uma trava própria, Lock trava = new();, e tranque com ela. Não tranque a variável que você quer proteger.
error CS1996: Não é possível aguardar no corpo de uma instrução lock
object trava = new();
lock (trava)
{
await Task.Delay(100);
}
$ dotnet run erro2.cs /home/ana/cs/lock/erro2.cs(4,5): error CS1996: Não é possível aguardar no corpo de uma instrução lock Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: não pode haver await dentro de um lock (em inglês, Cannot await in the body of a lock statement). Depois de um await, o resto do código pode continuar em outra thread, e a trava só pode ser devolvida pela thread que a pegou.
Como resolver: faça o await fora do lock (calcule o que precisa, tranque só para mexer nos dados). Se a parte protegida precisa mesmo esperar algo, use um SemaphoreSlim: await semaforo.WaitAsync(); try { … await … } finally { semaforo.Release(); }.
error CS4007: EnterScope com await
$ dotnet run escopo2.cs /home/ana/cs/lock/escopo2.cs(2,8): error CS4007: A instância do tipo "System.Threading.Lock.Scope" não pode ser preservada entre o limite "await" ou "yield". Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: é o mesmo problema do CS1996, escrito com using (trava.EnterScope()) { await Task.Delay(100); }. O compilador protege você também nessa forma.
Como resolver: o mesmo: tire o await de dentro, ou use SemaphoreSlim.
warning CS9216: um Lock guardado como object
object trava = new Lock();
lock (trava)
{
Console.WriteLine("dentro");
}
$ dotnet run aviso.cs /home/ana/cs/lock/aviso.cs(1,16): warning CS9216: Um valor do tipo "System.Threading.Lock" convertido em um tipo diferente usará o bloqueio baseado em monitor provavelmente não intencional na instrução "lock". dentro
O que aconteceu: o programa rodou, mas o compilador avisa: como a variável é do tipo object, o lock usa o mecanismo antigo (o Monitor) em vez dos métodos do Lock. Se uma parte do programa trancar pelo Lock e outra pelo object, elas não se excluem: são duas travas diferentes no mesmo objeto.
Como resolver: declare a variável (ou o campo) com o tipo certo: Lock trava = new();.
SynchronizationLockException: The calling thread does not hold the lock.
Lock trava = new(); trava.Exit();
$ dotnet run sair.cs; echo "código de saída: $?" Unhandled exception. System.Threading.SynchronizationLockException: The calling thread does not hold the lock. at System.Threading.Lock.Exit() at Program.<Main>$(String[] args) in /home/ana/cs/lock/sair.cs:line 2 código de saída: 134
O que aconteceu: o programa devolveu uma trava que não tinha pegado ("a thread que chamou não segura a trava"). Acontece com TryEnter/Exit escritos à mão: um Exit() sobrando, ou um Exit() chamado mesmo quando o TryEnter devolveu false.
Como resolver: prefira o lock (…), que nunca erra isso. Com TryEnter, chame Exit() só dentro do if em que ele deu true, num finally.
lock
O que aconteceu: não aparece mensagem nenhuma; o resultado só sai errado. As causas mais comuns:
- cada thread usa uma trava diferente (
lock (new object()), ou a trava criada dentro da lambda); - um lugar do código mexe na variável fora do
lock(por exemplo, umcontador = 0;esquecido em outro método); - a conferência e a ação estão em dois
lockseparados (um para oif, outro para oSaldo -= valor): entre eles, outra thread entra.
Como resolver: uma trava só, criada uma vez fora das tarefas (um campo private readonly), e todo acesso aos dados divididos dentro de um mesmo lock.
Referência rápida
| Código | O que faz |
|---|---|
private readonly Lock _trava = new(); | Cria a trava (C# 13 / .NET 9 em diante). |
private readonly object _trava = new(); | A trava na forma antiga (qualquer versão). |
lock (_trava) { … } | Só uma thread por vez roda o bloco; a trava é devolvida sempre, mesmo com exceção. |
using (_trava.EnterScope()) { … } | O mesmo que o lock, escrito à mão. |
_trava.TryEnter() | Tenta pegar a trava sem esperar: true ou false. |
_trava.TryEnter(TimeSpan.FromSeconds(1)) | Espera no máximo 1 segundo. |
_trava.Exit() | Devolve a trava (só depois de um TryEnter que deu true). |
_trava.IsHeldByCurrentThread | true se esta thread está com a trava. |
Interlocked.Increment(ref x) | Soma 1 de forma atômica (também Decrement, Add, Exchange). |
| Mensagem | Causa |
|---|---|
CS0185 | lock num tipo de valor (int, decimal, struct). |
CS1996 | await dentro de um lock. |
CS4007 | await dentro de um using (trava.EnterScope()). |
CS9216 (aviso) | Um Lock guardado numa variável object. |
SynchronizationLockException | Exit() sem ter a trava. |
Exercícios
1. Criar a trava
Complete o campo da trava de uma classe, no jeito recomendado do .NET 10:
2. Trancar
Complete a instrução que deixa só uma thread por vez somar ao total:
{
_total += valor;
}
3. Somar 1 sem lock
Complete a linha que soma 1 a visitas de forma atômica:
4. Um programa com 4 tarefas fazendo contador++, sem lock, deu o total certo na primeira vez que você rodou. O que isso prova?
5. Duas threads: uma faz lock (a) { lock (b) { … } }, a outra faz lock (b) { lock (a) { … } }. O programa às vezes trava e não termina. Qual é o conserto?
6. O método de saque confere o saldo e depois desconta. Onde vai o lock?
Missão final: o estoque da lojinha
Missão: a lojinha tem 10 cadernos e 20 clientes tentando comprar um cada, ao mesmo tempo. O método Vender confere e depois desconta, sem proteção. Na classe Estoque, crie o campo private readonly Lock _trava = new(); e ponha todo o corpo do Vender (o if inteiro) dentro de lock (_trava) { … }. A saída tem de ser Vendidos: 10 e Sobrou: 0.
var estoque = new Estoque(10);
var tarefas = new Task[20];
for (int i = 0; i < tarefas.Length; i++)
{
tarefas[i] = Task.Run(() => estoque.Vender());
}
await Task.WhenAll(tarefas);
Console.WriteLine($"Vendidos: {estoque.Vendidos}");
Console.WriteLine($"Sobrou: {estoque.Quantidade}");
class Estoque(int quantidade)
{
public int Quantidade { get; private set; } = quantidade;
public int Vendidos { get; private set; }
public void Vender()
{
if (Quantidade > 0)
{
Quantidade--;
Vendidos++;
}
}
}
var estoque = new Estoque(10);
var tarefas = new Task[20];
for (int i = 0; i < tarefas.Length; i++)
{
tarefas[i] = Task.Run(() => estoque.Vender());
}
await Task.WhenAll(tarefas);
Console.WriteLine($"Vendidos: {estoque.Vendidos}");
Console.WriteLine($"Sobrou: {estoque.Quantidade}");
class Estoque(int quantidade)
{
private readonly Lock _trava = new();
public int Quantidade { get; private set; } = quantidade;
public int Vendidos { get; private set; }
public void Vender()
{
lock (_trava)
{
if (Quantidade > 0)
{
Quantidade--;
Vendidos++;
}
}
}
}
Resumo
- Quando várias threads mexem na mesma variável, o resultado pode sair errado e diferente a cada execução: é a condição de corrida. Nem o compilador nem o programa avisam.
contador++são três passos (ler, somar, gravar); duas threads no meio deles perdem somas.lock (trava) { … }deixa só uma thread por vez dentro do bloco e devolve a trava sempre, mesmo com exceção.- No .NET 9 e no .NET 10, a trava recomendada é
private readonly Lock _trava = new();C# 13. A forma antiga usaobject. - "Confere e depois faz" (o saque, a venda) vai inteiro dentro do mesmo
lock. - Para uma operação só num número,
Interlocked.Increment(ref x)é mais leve. - Sem
awaitdentro dolock(CS1996); duas travas, sempre na mesma ordem, para evitar o impasse.