Qual é o custo da instrução de bloqueio?


111

Tenho feito experiências com multi-threading e processamento paralelo e precisava de um contador para fazer algumas contagens básicas e análises estatísticas da velocidade do processamento. Para evitar problemas com o uso simultâneo de minha classe, usei uma instrução de bloqueio em uma variável privada em minha classe:

private object mutex = new object();

public void Count(int amount)
{
 lock(mutex)
 {
  done += amount;
 }
}

Mas eu estava me perguntando ... quão caro é o bloqueio de uma variável? Quais são os efeitos negativos no desempenho?


10
Bloquear a variável não é tão caro; é a espera de uma variável bloqueada que você deseja evitar.
Gabe

53
é muito menos caro do que gastar horas rastreando outra condição de corrida ;-)
BrokenGlass

2
Bem ... se um bloqueio é caro, você pode querer evitá-los alterando a programação para que precise de menos bloqueios. Eu poderia implementar algum tipo de sincronização.
Kees C. Bakker

1
Tive uma melhora dramática no desempenho (agora, depois de ler o comentário de @Gabe) apenas removendo muito código dos meus blocos de bloqueio. Conclusão: de agora em diante, deixarei apenas o acesso à variável (geralmente uma linha) dentro de um bloco de bloqueio, uma espécie de "bloqueio just in time". Isso faz sentido?
heltonbiker

2
@heltonbiker Claro que faz sentido. Deve ser também um princípio arquitetônico, você deve fazer bloqueios o mais curto, simples e rápido possível. Somente dados realmente necessários que precisam ser sincronizados. Em servidores, você também deve levar em consideração a natureza híbrida do bloqueio. A contenção, mesmo que não seja crítica para o seu código, é graças à natureza híbrida do bloqueio, fazendo com que os núcleos girem durante cada acesso se o bloqueio for mantido por outra pessoa. Você está efetivamente devorando alguns recursos de cpu de outros serviços no servidor por algum tempo antes que seu thread seja suspenso.
ipavlu

Respostas:


86

Aqui está um artigo que analisa o custo. A resposta curta é 50 ns.


39
Melhor resposta curta: 50 ns + tempo gasto esperando se outro thread estiver bloqueado.
Herman

4
Quanto mais threads entram e saem do bloqueio, mais caro fica. O custo se expande exponencialmente com o número de threads
Arsen Zahray

16
Algum contexto: dividir dois números em um x86 de 3Ghz leva cerca de 10 ns (não incluindo o tempo que leva para buscar / decodificar a instrução) ; e carregar uma única variável da memória (não armazenada em cache) em um registro leva cerca de 40 ns. Então 50ns é insanamente, incrivelmente rápida - você não deve se preocupar com o custo de usar lockmais do que você se preocupar com o custo de usar uma variável.
BlueRaja - Danny Pflughoeft

3
Além disso, aquele artigo era antigo quando essa pergunta foi feita.
Otis

3
Métrica realmente ótima, "quase nenhum custo", para não mencionar incorreta. Vocês não levam em consideração que é curto e rápido apenas e SOMENTE se não houver nenhuma contenção, um tópico. NESSE CASO, VOCÊ NÃO PRECISA DE BLOQUEIO NENHUM. Segundo problema, o bloqueio não é bloqueio, mas bloqueio híbrido, detecta dentro do CLR que o bloqueio não é mantido por ninguém com base em operações atômicas e, nesse caso, evita chamadas para o núcleo do sistema operacional, que é um anel diferente que não é medido por estes testes. O que é medido como 25 ns a 50 ns é, na verdade, um código de instruções interligadas de nível de aplicativo se o bloqueio não for realizado
ipavlu

50

A resposta técnica é que isso é impossível de quantificar, pois depende muito do estado dos buffers de write-back da memória da CPU e de quantos dados que o pré-buscador reuniu devem ser descartados e relidos. Ambos são muito não determinísticos. Eu uso 150 ciclos de CPU como uma aproximação final que evita grandes decepções.

A resposta prática é que é muuuuito mais barato do que a quantidade de tempo que você gastará depurando seu código quando achar que pode pular um bloqueio.

Para obter um número rígido, você terá que medir. O Visual Studio tem um analisador de simultaneidade inteligente disponível como uma extensão.


1
Na verdade não, pode ser quantificado e medido. Simplesmente não é tão fácil quanto escrever esses bloqueios em todo o código e, em seguida, declarar que tudo é apenas 50 ns, um mito medido no acesso de thread único ao bloqueio.
ipavlu

8
"acho que você pode pular um bloqueio" ... Eu acho que é onde muitas pessoas estão quando lêem esta pergunta ...
Snoop

30

Leitura adicional:

Gostaria de apresentar alguns artigos meus, que estão interessados ​​em primitivas de sincronização gerais e estão se aprofundando no Monitor, comportamento da instrução de bloqueio C #, propriedades e custos, dependendo de cenários distintos e número de threads. Ele está especificamente interessado em desperdício de CPU e períodos de capacidade para entender quanto trabalho pode ser realizado em vários cenários:

https://www.codeproject.com/Articles/1236238/Unified-Concurrency-I-Introduction https://www.codeproject.com/Articles/1237518/Unified-Concurrency-II-benchmarking-methodologies https: // www. codeproject.com/Articles/1242156/Unified-Concurrency-III-cross-benchmarking

Resposta original:

Oh céus!

Parece que a resposta correta sinalizada aqui como A RESPOSTA é inerentemente incorreta! Gostaria de pedir ao autor da resposta, respeitosamente, que leia o link do artigo até o final. artigo

O autor do artigo, de 2003 artigo foi medição em única máquina Dual Core e, no primeiro caso de medição, ele medido bloqueio com apenas um único segmento eo resultado foi de cerca de 50 ns por acesso de bloqueio.

Não diz nada sobre um bloqueio no ambiente simultâneo. Portanto, temos que continuar lendo o artigo e na segunda metade, o autor estava medindo o cenário de bloqueio com dois e três threads, que se aproxima dos níveis de simultaneidade dos processadores de hoje.

Então o autor diz, que com dois threads no Dual Core, os bloqueios custam 120ns, e com 3 threads vai para 180ns. Portanto, parece ser claramente dependente do número de threads acessando o bloqueio simultaneamente.

Portanto, é simples, não é 50 ns a menos que seja um único thread, onde o bloqueio fica inútil.

Outra questão a considerar é que é medido como o tempo médio !

Se o tempo das iterações fosse medido, haveria tempos pares entre 1ms a 20ms, simplesmente porque a maioria foi rápida, mas poucos encadeamentos estarão esperando pelo tempo dos processadores e incorrerão em atrasos de até milissegundos.

Isso é uma má notícia para qualquer tipo de aplicativo que requeira alto rendimento e baixa latência.

E a última questão a ser considerada é que pode haver operações mais lentas dentro da fechadura e, muitas vezes, é esse o caso. Quanto mais tempo o bloco de código for executado dentro da fechadura, maior será a contenção e os atrasos serão muito altos.

Considere que já se passou mais de uma década desde 2003, ou seja, poucas gerações de processadores projetados especificamente para funcionar totalmente simultaneamente e o bloqueio está prejudicando consideravelmente seu desempenho.


1
Para esclarecer, o artigo não está dizendo que o desempenho do bloqueio degrada com o número de threads no aplicativo; o desempenho diminui com o número de threads disputando o bloqueio. (Isso está implícito, mas não claramente declarado, na resposta acima.)
Gooseberry

Presumo que você queira dizer isso: "Portanto, parece ser claramente dependente do número de threads acessados ​​simultaneamente e mais é pior." Sim, o texto poderia ser melhor. Eu quis dizer "acessados ​​simultaneamente" como threads acessando simultaneamente o bloqueio, criando contenção.
ipavlu

20

Isso não responde à sua consulta sobre desempenho, mas posso dizer que o .NET Framework oferece um Interlocked.Addmétodo que permitirá que você adicione seu amountao seu donemembro sem bloquear manualmente em outro objeto.


1
Sim, esta é provavelmente a melhor resposta. Mas principalmente por causa do código mais curto e limpo. A diferença na velocidade provavelmente não será perceptível.
Henk Holterman

obrigado por esta resposta. Estou fazendo mais coisas com fechaduras. Ints adicionados é um de muitos. Amei a sugestão, vou usá-la de agora em diante.
Kees C. Bakker,

Os bloqueios são muito mais fáceis de acertar, mesmo se o código sem bloqueio for potencialmente mais rápido. Interlocked.Add sozinho tem os mesmos problemas que + = sem sincronização.
hangar

10

lock (Monitor.Enter / Exit) é muito barato, mais barato do que alternativas como Waithandle ou Mutex.

Mas e se fosse (um pouco) lento, você preferisse um programa rápido com resultados incorretos?


5
Haha ... Eu estava buscando o programa rápido e os bons resultados.
Kees C. Bakker

@ henk-holterman Existem vários problemas com suas declarações: Primeiro, como esta pergunta e as respostas mostraram claramente, há pouca compreensão dos impactos do bloqueio no desempenho geral, até mesmo pessoas afirmando o mito sobre 50 ns que é aplicável apenas a ambientes de thread único. Em segundo lugar, sua declaração está aqui e permanecerá por anos e, nesse meio tempo, os processadores cresceram em núcleos, mas a velocidade dos núcleos não tanto. ** Três ** aplicativos tornam-se apenas mais complexos com o tempo, e então são camadas sobre camadas de bloqueando no ambiente de muitos núcleos e o número está aumentando,
2,4,8,10,20,16,32

Minha abordagem usual é construir a sincronização de forma fracamente acoplada com o mínimo de interação possível. Isso é muito rápido para estruturas de dados sem bloqueio. Fiz meus wrappers de código em torno do spinlock para simplificar o desenvolvimento e mesmo quando o TPL tem coleções simultâneas especiais, desenvolvi minhas próprias coleções de spin locked em torno de lista, array, dicionário e fila, pois precisava de um pouco mais de controle e às vezes algum código em execução sob spinlock. Eu posso te dizer, é possível e permite resolver vários cenários que as coleções do TPL não podem fazer e com grande ganho de desempenho / taxa de transferência.
ipavlu

7

O custo de um bloqueio em um loop apertado, em comparação com uma alternativa sem bloqueio, é enorme. Você pode fazer loops muitas vezes e ainda ser mais eficiente do que uma fechadura. É por isso que as filas sem bloqueio são tão eficientes.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LockPerformanceConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            var stopwatch = new Stopwatch();
            const int LoopCount = (int) (100 * 1e6);
            int counter = 0;

            for (int repetition = 0; repetition < 5; repetition++)
            {
                stopwatch.Reset();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++)
                    lock (stopwatch)
                        counter = i;
                stopwatch.Stop();
                Console.WriteLine("With lock: {0}", stopwatch.ElapsedMilliseconds);

                stopwatch.Reset();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++)
                    counter = i;
                stopwatch.Stop();
                Console.WriteLine("Without lock: {0}", stopwatch.ElapsedMilliseconds);
            }

            Console.ReadKey();
        }
    }
}

Resultado:

With lock: 2013
Without lock: 211
With lock: 2002
Without lock: 210
With lock: 1989
Without lock: 210
With lock: 1987
Without lock: 207
With lock: 1988
Without lock: 208

4
Este pode ser um mau exemplo porque o seu loop realmente não faz nada, exceto uma única atribuição de variável e um bloqueio é pelo menos 2 chamadas de função. Além disso, 20 ns por bloqueio que você está recebendo não é tão ruim.
Zar Shardan

5

Existem algumas maneiras diferentes de definir "custo". Existe a sobrecarga real de obter e liberar o bloqueio; como Jake escreve, isso é insignificante, a menos que essa operação seja executada milhões de vezes.

Mais relevante é o efeito que isso tem no fluxo de execução. Este código só pode ser inserido por um tópico de cada vez. Se você tiver 5 threads realizando essa operação regularmente, 4 deles vão acabar esperando que o bloqueio seja liberado e, então, será o primeiro thread agendado para inserir aquele trecho de código depois que o bloqueio for liberado. Portanto, seu algoritmo sofrerá significativamente. O quanto isso depende do algoritmo e da frequência com que a operação é chamada. Você realmente não pode evitá-la sem introduzir condições de corrida, mas pode melhorá-la minimizando o número de chamadas para o código bloqueado.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.