Tenho o seguinte programa, onde gero 1000 threads para incrementar uma variável compartilhada a e então faço cada thread dormir por 1 segundo:
using System;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Runtime.CompilerServices;
using System.Threading;
class Program
{
static volatile int a = 0;
static void Main()
{
List<Thread> threads = new List<Thread>();
for (int i = 0; i < 1000; i++)
{
var t = new Thread(() =>
{
a++;
Thread.Sleep(1000);
});
t.Start();
threads.Add(t);
}
// foreach(Thread t in threads) t.Join();
Thread.Sleep(60000);
Console.WriteLine(a);
Console.ReadKey();
}
}
Problema:
Quando executo este código com a linha Thread.Sleep(1000) incluída e t.Join() comentado, a saída de Console.WriteLine(a) é menor que 1000, mesmo que eu aguarde 60 segundos para que os threads terminem.
Se eu descomentar o loop t.Join() ou comentar a linha Thread.Sleep(1000), a saída será consistentemente 1000.
Questões:
Por que a saída varia quando Thread.Sleep(1000) está presente e t.Join() está comentado?
Por que a saída se torna consistente (sempre 1000) quando t.Join() é usado ou Thread.Sleep(1000) é removido?
Eu apreciaria uma explicação do que está acontecendo aqui em termos de comportamento de thread e sincronização. Obrigado!
TBH não foi capaz de reproduzir o efeito
foreach(Thread t in threads) t.Join();
vsThread.Sleep
- ambos consistentemente (em um número relativamente pequeno de tentativas, no entanto) dão a soma "esperada" (talvez haja alguma diferença de configuração que faça com que nem todos os threads consigam completar, embora issoThread.Sleep(60000)
seja muito duvidoso). Eu sugeriria alterar o código para:ou seja, altere a ordem das operações, então o
foreach(Thread t in threads) t.Join();
truque vai parar de funcionar (pelo menos "na minha máquina"). O fato de que foi uma mera coincidência, eu diria que relacionado ao fato de quea++
é uma operação relativamente rápida e, como você está iniciando o thread antes de adicioná-lo à lista e criar o próximo, ele tem tempo suficiente para ser concluído.Alterar a ordem de suspensão e incremento traz (pelo menos para mim) o problema principal à tona: incremento (
a++
) não é atômico , e usá-lo em contexto multithread torna seu programa inválido . Você deve usar alguma sincronização ou versão atômica da operação (se houver).Por exemplo,
Interlocked.Increment
tornará seu programa correto:Eu também recomendo conferir:
O SO pode agendar threads sempre que desejar. Então, é provável que nem todas as threads tenham tido a chance de rodar. Como o SO não fornece nenhuma garantia de tempo específica, você não pode confiar apenas em esperar para garantir que as threads rodaram.
Join bloqueia a execução do thread atual até que o thread de junção tenha sido concluído. Então, se você juntar todos os threads, você pode ter certeza de que todos os threads concluíram a execução.
Mas isso não significa que
a
seja garantido que seja 1000. É possível que o primeiro thread:a
como0
a
para1
1
de memóriaResultar em uma saída de 1
volatile
não impede isso, você precisaria de um bloqueio, ou Interlocked.Increment para incrementar uma variável com segurança.Multithreading é difícil , e você não pode usar testes como este para verificar se um comportamento está correto. Um teste pode ser bem-sucedido um milhão de vezes, mas falhar em outro computador, ou quando há uma carga alta, ou quando há lua cheia. Na melhor das hipóteses, você pode verificar se um comportamento está incorreto . Você precisa saber os perigos e como resolvê-los. As melhores práticas de threading gerenciado são um bom começo, mas há muitos outros bons artigos sobre segurança de thread.