Qual é a diferença entre mutex e seção crítica?

134

Por favor, explique a partir das perspectivas do Linux, Windows?

Estou programando em c #, esses dois termos fariam diferença. Por favor, poste o máximo que puder, com exemplos e coisas assim ....

obrigado


fonte

Respostas:

232

Para Windows, as seções críticas são mais leves que as mutexes.

Mutexes podem ser compartilhados entre processos, mas sempre resultam em uma chamada do sistema ao kernel que possui alguma sobrecarga.

As seções críticas podem ser usadas apenas dentro de um processo, mas têm a vantagem de mudar apenas para o modo kernel no caso de contenção - aquisições não contidas, que devem ser o caso comum, são incrivelmente rápidas. No caso de contenção, eles entram no kernel para aguardar alguma primitiva de sincronização (como um evento ou semáforo).

Eu escrevi um aplicativo de amostra rápida que compara o tempo entre os dois. No meu sistema, para 1.000.000 de aquisições e lançamentos independentes, um mutex leva mais de um segundo. Uma seção crítica leva ~ 50 ms para 1.000.000 de aquisições.

Aqui está o código de teste, eu executei isso e obtive resultados semelhantes se o mutex for o primeiro ou o segundo, então não estamos vendo nenhum outro efeito.

HANDLE mutex = CreateMutex(NULL, FALSE, NULL);
CRITICAL_SECTION critSec;
InitializeCriticalSection(&critSec);

LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);
LARGE_INTEGER start, end;

// Force code into memory, so we don't see any effects of paging.
EnterCriticalSection(&critSec);
LeaveCriticalSection(&critSec);
QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    EnterCriticalSection(&critSec);
    LeaveCriticalSection(&critSec);
}

QueryPerformanceCounter(&end);

int totalTimeCS = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);

// Force code into memory, so we don't see any effects of paging.
WaitForSingleObject(mutex, INFINITE);
ReleaseMutex(mutex);

QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    WaitForSingleObject(mutex, INFINITE);
    ReleaseMutex(mutex);
}

QueryPerformanceCounter(&end);

int totalTime = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);

printf("Mutex: %d CritSec: %d\n", totalTime, totalTimeCS);
Michael
fonte
1
Não tenho certeza se isso está relacionado ou não (já que eu não compilei e tentei seu código), mas descobri que chamar WaitForSingleObject com INFINITE resulta em desempenho ruim. Passar um valor de tempo limite de 1 e fazer um loop enquanto verifica seu retorno fez uma enorme diferença no desempenho de alguns dos meus códigos. No entanto, isso ocorre principalmente no contexto de aguardar um identificador de processo externo ... Não é um mutex. YMMV. Eu estaria interessado em ver como o mutex se comporta com essa modificação. A diferença de horário resultante deste teste parece maior do que o esperado.
Troy Howard
5
@TroyHoward você não está basicamente apenas girando o bloqueio nesse ponto?
dss539
As razões para essa distinção são provavelmente históricas. Não é difícil implementar o bloqueio que é tão rápido quanto CriticalSection no caso não contido (poucas instruções atômicas, sem syscalls), mas funciona em todos os processos (com um pedaço de memória compartilhada). Veja, por exemplo, futexes do Linux .
regnarg
2
@TroyHoward tente forçar sua CPU a funcionar a 100% o tempo todo e veja se o INFINITE funciona melhor. A estratégia de energia pode demorar até 40ms na minha máquina (Dell XPS-8700) para voltar à velocidade máxima depois que ela decidir diminuir a velocidade, o que pode não acontecer se você dormir ou esperar apenas um milissegundo.
Stevens Miller
Não sei se entendi o que está sendo demonstrado aqui. Geralmente, entrar em uma seção crítica requer a aquisição de algum tipo de semáforo. Você está dizendo que nos bastidores, o sistema operacional tem uma maneira eficiente de implementar esse comportamento crítico da seção sem exigir mutexes?
SN
89

De uma perspectiva teórica, uma seção crítica é um pedaço de código que não deve ser executado por vários encadeamentos de uma só vez, porque o código acessa recursos compartilhados.

Um mutex é um algoritmo (e algumas vezes o nome de uma estrutura de dados) usado para proteger seções críticas.

Semáforos e monitores são implementações comuns de um mutex.

Na prática, existem muitas implementações mutex disponíveis no Windows. Eles diferem principalmente como conseqüência de sua implementação por seu nível de bloqueio, escopos, custos e desempenho em diferentes níveis de contenção. Consulte CLR Inside Out - Usando simultaneidade para escalabilidade para obter um gráfico dos custos de diferentes implementações de mutex.

Primitivas de sincronização disponíveis.

A lock(object)declaração é implementada usando um Monitor- consulte MSDN para referência.

Nos últimos anos, muita pesquisa é feita sobre sincronização sem bloqueio . O objetivo é implementar algoritmos de maneira livre de bloqueio ou sem espera. Nesses algoritmos, um processo ajuda outros processos a finalizar seu trabalho, para que o processo possa finalmente terminar seu trabalho. Em conseqüência, um processo pode terminar seu trabalho mesmo quando outros processos, que tentaram executar algum trabalho, travam. Usando bloqueios, eles não liberariam seus bloqueios e impediriam a continuidade de outros processos.

Daniel Brückner
fonte
Vendo a resposta aceita, pensei que talvez me lembrasse errado do conceito de seções críticas, até ver a Perspectiva Teórica que você escreveu. :)
Anirudh Ramanathan
2
A programação prática de bloqueio livre é como Shangri La, exceto que existe. O artigo de Keir Fraser (PDF) explora isso de maneira bastante interessante (desde 2004). E ainda estamos lutando com isso em 2012. Nós somos péssimos.
Tim Post
22

Além das outras respostas, os seguintes detalhes são específicos para seções críticas no Windows:

  • na ausência de contenção, adquirir uma seção crítica é tão simples quanto uma InterlockedCompareExchangeoperação
  • a estrutura da seção crítica abre espaço para um mutex. Inicialmente não alocado
  • se houver contenção entre os threads para uma seção crítica, o mutex será alocado e usado. O desempenho da seção crítica se degradará ao do mutex
  • se você antecipar alta contenção, poderá alocar a seção crítica especificando uma contagem de giros.
  • se houver uma contenção em uma seção crítica com uma contagem de giros, o encadeamento que tentar adquirir a seção crítica será girado (espera ocupada) por tantos ciclos de processador. Isso pode resultar em melhor desempenho do que no modo de suspensão, pois o número de ciclos para executar uma alternância de contexto para outro encadeamento pode ser muito maior que o número de ciclos executados pelo encadeamento proprietário para liberar o mutex
  • se a contagem de giros expirar, o mutex será alocado
  • quando o encadeamento proprietário libera a seção crítica, é necessário verificar se o mutex está alocado; se for, ele definirá o mutex para liberar um encadeamento em espera

No linux, acho que eles têm um "bloqueio de rotação" que serve a um propósito semelhante à seção crítica com uma contagem de rotação.

1800 INFORMAÇÃO
fonte
Infelizmente, uma seção crítica do Windows envolve executar uma operação CAS no modo kernel , que é massivamente mais caro que a operação intertravada real. Além disso, as seções críticas do Windows podem ter contagens de rotação associadas a elas.
Promit
2
Isso definitivamente não é verdade. O CAS pode ser feito com cmpxchg no modo de usuário.
Michael
Eu pensei que a contagem de giros padrão fosse zero se você chamasse InitializeCriticalSection - você deve chamar InitializeCriticalSectionAndSpinCount se quiser que uma contagem de giros seja aplicada. Você tem uma referência para isso?
1800 INFORMAÇÕES
18

Seção crítica e Mutex não são específicos do sistema operacional, seus conceitos de multithreading / multiprocessing.

Seção crítica É um pedaço de código que só deve ser executado a qualquer momento (por exemplo, há 5 threads em execução simultaneamente e uma função chamada "critical_section_function" que atualiza uma matriz ... você não deseja todos os 5 threads atualizando a matriz de uma vez.Portanto, quando o programa estiver executando critical_section_function (), nenhum dos outros threads deve executar sua critical_section_function.

mutex * O mutex é uma maneira de implementar o código de seção crítica (pense nele como um token ... o thread deve ter posse dele para executar o critical_section_code)

O desconhecido
fonte
2
Além disso, mutexes podem ser compartilhados entre processos.
Configurator
14

Um mutex é um objeto que um thread pode adquirir, impedindo que outros threads o adquiram. É consultivo, não obrigatório; um encadeamento pode usar o recurso que o mutex representa sem adquiri-lo.

Uma seção crítica é um comprimento de código garantido pelo sistema operacional para não ser interrompido. No pseudo-código, seria como:

StartCriticalSection();
    DoSomethingImportant();
    DoSomeOtherImportantThing();
EndCriticalSection();
Zifre
fonte
1
Acho que o pôster estava falando sobre primitivas de sincronização no modo de usuário, como um objeto de seção Crítica do win32, que apenas fornece exclusão mútua. Eu não sei sobre Linux, mas o kernel do Windows tem regiões críticas que se comportam como você descreve - ininterrupta.
Michael
1
Não sei por que você foi derrotado. Existe o conceito de uma seção crítica, que você descreveu corretamente, diferente do objeto do kernel do Windows chamado CriticalSection, que é um tipo de mutex. Acredito que o OP estava perguntando sobre a última definição.
23411 Adam Rosenfield
Pelo menos fiquei confuso com a etiqueta agnóstica do idioma. De qualquer forma, é o que obtemos para a Microsoft nomear sua implementação da mesma forma que sua classe base. Má prática de codificação!
Mikko Rantanen
Bem, ele pediu o máximo de detalhes possível e disse especificamente que o Windows e o Linux parecem bons conceitos. +1 - também não entendeu o -1: /
Jason Coco
14

O Windows 'rápido' igual à seleção crítica no Linux seria um futex , que significa mutex rápido no espaço do usuário. A diferença entre um futex e um mutex é que, com um futex, o kernel somente se envolve quando a arbitragem é necessária, portanto, você economiza a sobrecarga de conversar com o kernel cada vez que o contador atômico é modificado. Isso pode economizar uma quantidade significativa de tempo negociando bloqueios em alguns aplicativos.

Um futex também pode ser compartilhado entre processos, usando os meios que você empregaria para compartilhar um mutex.

Infelizmente, os futexes podem ser muito difíceis de implementar (PDF). (Atualização de 2018, eles não são tão assustadores quanto em 2009).

Além disso, é praticamente o mesmo nas duas plataformas. Você está fazendo atualizações atômicas e controladas por token em uma estrutura compartilhada de uma maneira que (espero) não cause fome. O que resta é simplesmente o método de conseguir isso.

Tim Post
fonte
6

No Windows, uma seção crítica é local para o seu processo. Um mutex pode ser compartilhado / acessado entre processos. Basicamente, as seções críticas são muito mais baratas. Não é possível comentar especificamente sobre o Linux, mas em alguns sistemas eles são apenas aliases para a mesma coisa.

Promit
fonte
6

Apenas para adicionar meus 2 centavos, as Seções críticas são definidas como uma estrutura e as operações são realizadas no contexto do modo de usuário.

ntdll! _RTL_CRITICAL_SECTION
   + 0x000 DebugInfo: Ptr32 _RTL_CRITICAL_SECTION_DEBUG
   + 0x004 LockCount: Int4B
   + 0x008 RecursionCount: Int4B
   + 0x00c OwningThread: Ptr32 Vazio
   + 0x010 LockSemaphore: Ptr32 Vazio
   + 0x014 SpinCount: Uint4B

Enquanto mutex são objetos de kernel (ExMutantObjectType) criados no diretório de objetos do Windows. As operações do Mutex são implementadas principalmente no modo kernel. Por exemplo, ao criar um Mutex, você acaba chamando nt! NtCreateMutant no kernel.

Martin
fonte
O que acontece quando um programa que inicializa e usa um objeto Mutex falha? O objeto Mutex é desalocado automaticamente? Não, eu diria. Certo?
Ankur
6
Os objetos do kernel têm uma contagem de referência. Fechar um identificador para um objeto diminui a contagem de referência e quando atinge 0, o objeto é liberado. Quando um processo falha, todos os seus identificadores são fechados automaticamente, portanto, um mutex ao qual somente esse processo possui um identificador seria automaticamente desalocado.
187 Michael Michael
E é por essa razão que os objetos da Seção Crítica são vinculados ao processo; por outro lado, mutexes podem ser compartilhados entre os processos.
Sisir 27/01/19
2

Ótima resposta de Michael. Adicionei um terceiro teste para a classe mutex introduzida no C ++ 11. O resultado é um pouco interessante e ainda suporta o endosso original de objetos CRITICAL_SECTION para processos únicos.

mutex m;
HANDLE mutex = CreateMutex(NULL, FALSE, NULL);
CRITICAL_SECTION critSec;
InitializeCriticalSection(&critSec);

LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);
LARGE_INTEGER start, end;

// Force code into memory, so we don't see any effects of paging.
EnterCriticalSection(&critSec);
LeaveCriticalSection(&critSec);
QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    EnterCriticalSection(&critSec);
    LeaveCriticalSection(&critSec);
}

QueryPerformanceCounter(&end);

int totalTimeCS = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);

// Force code into memory, so we don't see any effects of paging.
WaitForSingleObject(mutex, INFINITE);
ReleaseMutex(mutex);

QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    WaitForSingleObject(mutex, INFINITE);
    ReleaseMutex(mutex);
}

QueryPerformanceCounter(&end);

int totalTime = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);

// Force code into memory, so we don't see any effects of paging.
m.lock();
m.unlock();

QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    m.lock();
    m.unlock();
}

QueryPerformanceCounter(&end);

int totalTimeM = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);


printf("C++ Mutex: %d Mutex: %d CritSec: %d\n", totalTimeM, totalTime, totalTimeCS);

Meus resultados foram 217, 473 e 19 (observe que minha proporção de vezes nos últimos dois é comparável à de Michael, mas minha máquina é pelo menos quatro anos mais nova que a dele, para que você possa ver evidências de aumento de velocidade entre 2009 e 2013 , quando o XPS-8700 saiu). A nova classe mutex é duas vezes mais rápida que o Windows mutex, mas ainda menos que um décimo da velocidade do objeto Windows CRITICAL_SECTION. Observe que eu testei apenas o mutex não recursivo. Os objetos CRITICAL_SECTION são recursivos (um thread pode inseri-los repetidamente, desde que deixe o mesmo número de vezes).

Stevens Miller
fonte
0

As funções CA são chamadas reentrantes se usar apenas seus parâmetros reais.

As funções reentrantes podem ser chamadas por vários threads ao mesmo tempo.

Exemplo de função reentrante:

int reentrant_function (int a, int b)
{
   int c;

   c = a + b;

   return c;
}

Exemplo de função não reentrante:

int result;

void non_reentrant_function (int a, int b)
{
   int c;

   c = a + b;

   result = c;

}

A biblioteca padrão C strtok () não é reentrada e não pode ser usada por 2 ou mais threads ao mesmo tempo.

Alguns SDKs de plataforma vêm com a versão reentrante do strtok () chamada strtok_r ();

Enrico Migliore

Enrico Migliore
fonte