Eu queria saber a mesma coisa, então medi-a. Na minha caixa (processador AMD FX (tm) -8150 de oito núcleos a 3.612361 GHz), bloquear e desbloquear um mutex desbloqueado que está em sua própria linha de cache e já está armazenado em cache leva 47 relógios (13 ns).
Devido à sincronização entre dois núcleos (usei a CPU n ° 0 e n ° 1), eu só poderia chamar um par de bloqueio / desbloqueio uma vez a cada 102 ns em dois threads, assim como uma vez a cada 51 ns, do qual se pode concluir que são necessários aproximadamente 38 ns para recuperar depois que um thread faz um desbloqueio antes que o próximo thread possa bloqueá-lo novamente.
O programa que eu usei para investigar isso pode ser encontrado aqui:
https://github.com/CarloWood/ai-statefultask-testsuite/blob/b69b112e2e91d35b56a39f41809d3e3de2f9e4b8/src/mutex_test.cxx
Observe que ele possui alguns valores codificados específicos para minha caixa (xrange, yrange e rdtsc overhead), portanto, você provavelmente precisará experimentá-la antes que ela funcione para você.
O gráfico que produz nesse estado é:
Isso mostra o resultado das execuções de benchmark no seguinte código:
uint64_t do_Ndec(int thread, int loop_count)
{
uint64_t start;
uint64_t end;
int __d0;
asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (start) : : "%rdx");
mutex.lock();
mutex.unlock();
asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (end) : : "%rdx");
asm volatile ("\n1:\n\tdecl %%ecx\n\tjnz 1b" : "=c" (__d0) : "c" (loop_count - thread) : "cc");
return end - start;
}
As duas chamadas rdtsc medem o número de relógios necessários para bloquear e desbloquear o `mutex '(com uma sobrecarga de 39 relógios para as chamadas rdtsc na minha caixa). O terceiro asm é um loop de atraso. O tamanho do loop de atraso é 1 contagem menor para o segmento 1 do que para o segmento 0, portanto, o segmento 1 é um pouco mais rápido.
A função acima é chamada em um loop apertado de tamanho 100.000. Apesar de a função ser um pouco mais rápida para o encadeamento 1, os dois loops são sincronizados devido à chamada para o mutex. Isso é visível no gráfico pelo fato de o número de relógios medidos para o par de bloqueio / desbloqueio ser um pouco maior para o encadeamento 1, para explicar o menor atraso no loop abaixo dele.
No gráfico acima, o ponto inferior direito é uma medida com um atraso loop_count de 150 e, seguindo os pontos na parte inferior, em direção à esquerda, o loop_count é reduzido em um a cada medição. Quando se torna 77, a função é chamada a cada 102 ns nos dois threads. Se subsequentemente loop_count for reduzido ainda mais, não será mais possível sincronizar os encadeamentos e o mutex começará a ser realmente bloqueado a maior parte do tempo, resultando em uma quantidade maior de relógios necessários para o bloqueio / desbloqueio. Além disso, o tempo médio da chamada da função aumenta por causa disso; então os pontos da trama agora sobem e voltam para a direita.
A partir disso, podemos concluir que bloquear e desbloquear um mutex a cada 50 ns não é um problema na minha caixa.
Em suma, minha conclusão é que a resposta à pergunta do OP é que adicionar mais mutexes é melhor, desde que isso resulte em menos contenção.
Tente bloquear os mutexes o mais curto possível. O único motivo para colocá-los - digamos - fora de um loop seria se esse loop fizesse loops mais rápido que uma vez a cada 100 ns (ou melhor, número de threads que desejam executar esse loop ao mesmo tempo 50 ns) ou 13 vezes ns o tamanho do loop é mais atraso do que o atraso que você recebe por contenção.
Edição: Eu tenho muito mais conhecimento sobre o assunto agora e começo a duvidar da conclusão que apresentei aqui. Primeiro de tudo, a CPU 0 e 1 são hiperencadeadas; embora a AMD afirme ter 8 núcleos reais, certamente há algo muito suspeito porque os atrasos entre outros dois núcleos são muito maiores (ou seja, 0 e 1 formam um par, como 2 e 3, 4 e 5 e 6 e 7 ) Em segundo lugar, o std :: mutex é implementado de forma a girar os bloqueios um pouco antes de realmente fazer chamadas do sistema quando falha em obter imediatamente o bloqueio em um mutex (o que sem dúvida será extremamente lento). Portanto, o que eu medi aqui é a situação ideal mais absoluta e, na prática, o bloqueio e desbloqueio podem levar drasticamente mais tempo por bloqueio / desbloqueio.
Bottom line, um mutex é implementado com atômica. Para sincronizar átomos entre núcleos, um barramento interno deve ser bloqueado, o que congela a linha de cache correspondente por várias centenas de ciclos de clock. No caso de não ser possível obter um bloqueio, é necessário executar uma chamada do sistema para colocar o encadeamento em suspensão; isso é obviamente extremamente lento (as chamadas do sistema são da ordem de 10 mircosegundos). Normalmente, isso não é realmente um problema, porque esse segmento precisa dormir de qualquer maneira - mas pode ser um problema com alta contenção, em que um segmento não pode obter o bloqueio pelo tempo em que normalmente gira e o sistema chama, mas PODE pegue a fechadura logo depois. Por exemplo, se vários encadeamentos bloqueiam e desbloqueiam um mutex em um loop apertado e cada um mantém o bloqueio por 1 microssegundo, então eles podem ser desacelerados enormemente pelo fato de serem constantemente adormecidos e acordados novamente. Além disso, uma vez que um thread dorme e outro thread precise ativá-lo, esse thread precisa fazer uma chamada de sistema e atrasa ~ 10 microssegundos; esse atraso ocorre durante o desbloqueio de um mutex quando outro encadeamento aguarda esse mutex no kernel (após a rotação demorou muito).