Bloqueio recursivo (Mutex) vs Bloqueio não recursivo (Mutex)

183

POSIX permite que mutexes sejam recursivos. Isso significa que o mesmo encadeamento pode bloquear o mesmo mutex duas vezes e não entra em conflito. Obviamente, ele também precisa desbloqueá-lo duas vezes, caso contrário, nenhum outro thread pode obter o mutex. Nem todos os sistemas que suportam pthreads também suportam mutexes recursivos, mas se eles desejam estar em conformidade com POSIX, eles precisam .

Outras APIs (APIs de mais alto nível) também costumam oferecer mutexes, geralmente chamadas de bloqueios. Alguns sistemas / idiomas (por exemplo, Cocoa Objective-C) oferecem mutexes recursivos e não recursivos. Alguns idiomas também oferecem apenas um ou outro. Por exemplo, em Java, os mutexes são sempre recursivos (o mesmo thread pode "sincronizar" duas vezes no mesmo objeto). Dependendo de outras funcionalidades de encadeamento que eles oferecem, não ter mutexes recursivos pode não ser um problema, pois eles podem ser escritos facilmente (eu já implementei os mutexes recursivos com base em operações mais simples de mutex / condição).

O que realmente não entendo: Para que servem os mutexes não recursivos? Por que eu gostaria de ter um bloqueio de thread se ele bloqueia o mesmo mutex duas vezes? Mesmo linguagens de alto nível que poderiam evitar isso (por exemplo, testar se isso causará um impasse e lançar uma exceção, se houver) geralmente não fazem isso. Em vez disso, eles deixarão o bloqueio do encadeamento.

Isso é apenas para casos em que eu bloqueio acidentalmente duas vezes e desbloqueio apenas uma vez e no caso de um mutex recursivo, seria mais difícil encontrar o problema; portanto, tenho um bloqueio imediatamente para ver onde aparece o bloqueio incorreto? Mas não consegui fazer o mesmo com a devolução de um contador de bloqueio ao desbloquear e em uma situação, onde tenho certeza de que soltei o último bloqueio e o contador não é zero, posso lançar uma exceção ou registrar o problema? Ou há outro caso de uso mais útil de mutexes não recursivos que eu falho em ver? Ou talvez seja apenas desempenho, pois um mutex não recursivo pode ser um pouco mais rápido que um recursivo? No entanto, testei isso e a diferença não é tão grande assim.

Mecki
fonte

Respostas:

154

A diferença entre um mutex recursivo e não recursivo tem a ver com propriedade. No caso de um mutex recursivo, o kernel precisa acompanhar o thread que realmente obteve o mutex na primeira vez, para que ele possa detectar a diferença entre recursão e um thread diferente que deve bloquear. Como outra resposta apontada, há uma questão de sobrecarga adicional disso, tanto em termos de memória para armazenar esse contexto quanto nos ciclos necessários para mantê-lo.

No entanto , há outras considerações em jogo aqui também.

Como o mutex recursivo tem um senso de propriedade, o thread que agarra o mutex deve ser o mesmo thread que libera o mutex. No caso de mutexes não recursivos, não há senso de propriedade e qualquer encadeamento geralmente pode liberar o mutex, não importa qual encadeamento originalmente tenha o mutex. Em muitos casos, esse tipo de "mutex" é realmente mais uma ação de semáforo, em que você não está necessariamente usando o mutex como um dispositivo de exclusão, mas como um dispositivo de sincronização ou sinalização entre dois ou mais threads.

Outra propriedade que vem com um senso de propriedade em um mutex é a capacidade de suportar a herança de prioridade. Como o kernel pode rastrear o encadeamento que possui o mutex e também a identidade de todos os bloqueadores, em um sistema de encadeamento prioritário, é possível escalar a prioridade do encadeamento que atualmente possui o mutex para a prioridade do encadeamento de maior prioridade. que está atualmente bloqueando o mutex. Essa herança evita o problema de inversão de prioridade que pode ocorrer nesses casos. (Observe que nem todos os sistemas suportam herança prioritária em tais mutexes, mas é outro recurso que se torna possível através da noção de propriedade).

Se você se referir ao kernel clássico do VxWorks RTOS, eles definem três mecanismos:

  • mutex - suporta recursão e, opcionalmente, herança prioritária. Esse mecanismo é comumente usado para proteger seções críticas de dados de maneira coerente.
  • semáforo binário - sem recursão, sem herança, exclusão simples, tomador e doador não precisa ser o mesmo encadeamento, liberação de transmissão disponível. Esse mecanismo pode ser usado para proteger seções críticas, mas também é particularmente útil para sinalização coerente ou sincronização entre threads.
  • semáforo de contagem - sem recursão ou herança, atua como um contador de recursos coerente a partir de qualquer contagem inicial desejada; os encadeamentos bloqueiam apenas onde a contagem líquida no recurso é zero.

Novamente, isso varia de acordo com a plataforma - especialmente o que eles chamam de coisas, mas deve ser representativo dos conceitos e dos vários mecanismos em jogo.

Tall Jeff
fonte
9
sua explicação sobre mutex não recursivo parecia mais um semáforo. Um mutex (seja recursivo ou não recursivo) tem uma noção de propriedade.
Jay D
@ JayD É muito confuso quando as pessoas discutem sobre coisas como essas. Então, quem é a entidade que define essas coisas?
Pacerier 8/12
13
@Pacerier A norma relevante. Essa resposta está errada, por exemplo, no posix (pthreads), onde desbloquear um mutex normal em um thread que não seja o que o bloqueou é um comportamento indefinido, enquanto faz o mesmo com uma verificação de erro ou mutex recursivo resulta em um código de erro previsível. Outros sistemas e padrões podem se comportar muito diferentes.
nºs
Talvez isso seja ingênuo, mas fiquei com a impressão de que a idéia central de um mutex é que o segmento de bloqueio desbloqueia o mutex e outros segmentos podem fazer o mesmo. De computação.llnl.gov
tutorials
2
@curiousguy - uma liberação de transmissão libera todo e qualquer encadeamento bloqueado no semáforo sem explicitamente (permanece vazio), enquanto um normal binário liberaria apenas o encadeamento no início da fila de espera (supondo que haja um bloqueado).
Tall Jeff
123

A resposta não é eficiência. Mutexes não reentrantes levam a um melhor código.

Exemplo: A :: foo () adquire o bloqueio. Em seguida, chama B :: bar (). Isso funcionou bem quando você o escreveu. Mas algum tempo depois alguém muda B :: bar () para chamar A :: baz (), que também adquire o bloqueio.

Bem, se você não tem mutexes recursivos, esses impasses. Se você os tiver, ele será executado, mas poderá quebrar. A :: foo () pode ter deixado o objeto em um estado inconsistente antes de chamar bar (), supondo que baz () não pôde ser executado porque também adquire o mutex. Mas provavelmente não deveria correr! A pessoa que escreveu A :: foo () assumiu que ninguém poderia chamar A :: baz () ao mesmo tempo - essa é toda a razão pela qual ambos os métodos adquiriram o bloqueio.

O modelo mental certo para usar mutexes: O mutex protege um invariante. Quando o mutex é mantido, o invariante pode mudar, mas antes de liberar o mutex, o invariante é restabelecido. Os bloqueios de reentrada são perigosos porque, na segunda vez que você adquire o bloqueio, não pode mais ter certeza de que o invariante é verdadeiro.

Se você está satisfeito com os bloqueios de reentrada, é apenas porque você não precisou depurar um problema como esse antes. Atualmente, o Java tem bloqueios não reentrantes em java.util.concurrent.locks.

Jonathan
fonte
4
Demorei um pouco para entender o que você estava dizendo sobre o invariante não ser válido quando você fecha a fechadura pela segunda vez. Bom ponto! E se fosse um bloqueio de leitura e gravação (como o ReadWriteLock do Java) e você adquiriu o bloqueio de leitura e depois adquiriu novamente o bloqueio de leitura uma segunda vez no mesmo encadeamento. Você não invalidaria um invariante depois de adquirir um bloqueio de leitura, certo? Portanto, quando você adquire o segundo bloqueio de leitura, o invariante ainda é verdadeiro.
dgrant
1
@ Jonathan Jonathan O Java tem bloqueios não reentrantes hoje em java.util.concurrent.locks ?
user454322
1
+1 Acho que o uso mais comum para o bloqueio de reentrada está dentro de uma única classe, na qual alguns métodos podem ser chamados a partir de partes de código protegidas e não protegidas. Na verdade, isso sempre pode ser fatorado. @ user454322 Claro Semaphore,.
maaartinus 27/09/14
1
Perdoe meu mal-entendido, mas não vejo como isso é relevante para o mutex. Suponha que não haja multithreading e bloqueio envolvido, A::foo()ainda pode ter deixado o objeto em um estado inconsistente antes de chamar A::bar(). O que mutex, recursivo ou não, tem algo a ver com este caso?
Siyuan Ren
1
@SiyuanRen: O problema está sendo capaz de raciocinar localmente sobre o código. As pessoas (pelo menos eu) são treinadas para reconhecer regiões bloqueadas como manutenção invariável, ou seja, no momento em que você adquire o bloqueio, nenhum outro encadeamento está modificando o estado, de modo que os invariantes na região crítica se mantêm. Essa não é uma regra difícil, e você pode codificar com os invariantes não sendo lembrados, mas isso tornaria seu código mais difícil de raciocinar e manter. O mesmo acontece no modo de thread único sem mutexes, mas não somos treinados para raciocinar localmente em torno da região protegida.
David Rodríguez - dribeas
92

Como escrito pelo próprio Dave Butenhof :

"O maior de todos os grandes problemas com mutexes recursivos é que eles o incentivam a perder completamente o controle de seu esquema e escopo de bloqueio. Isso é mortal. Mal. É o" comedor de threads ". Você mantém os bloqueios pelo menor tempo possível. Período: sempre. Se você está chamando algo com uma trava mantida simplesmente porque não sabe que ela está retida, ou porque não sabe se o receptor precisa do mutex, então está esperando por muito tempo. apontando uma espingarda para o seu aplicativo e pressionando o gatilho. Você provavelmente começou a usar threads para obter simultaneidade; mas acabou de PREVENIR a simultaneidade. "

Chris Cleeland
fonte
9
Observe também a parte final da resposta de Butenhof: ...you're not DONE until they're [recursive mutex] all gone.. Or sit back and let someone else do the design.
user454322
2
Ele também diz que usar um único mutex recursivo global (sua opinião é que você precisa de apenas um) é uma muleta para adiar conscientemente o trabalho árduo de entender as invariâncias de uma biblioteca externa quando você começa a usá-lo em código multithread. Mas você não deve usar muletas para sempre, mas eventualmente investe tempo para entender e corrigir os invariantes de simultaneidade do código. Então, podemos parafrasear que o uso de mutex recursivo é uma dívida técnica.
Foof
13

O modelo mental certo para usar mutexes: O mutex protege um invariante.

Por que você tem certeza de que esse é realmente o modelo mental correto para o uso de mutexes? Eu acho que o modelo certo está protegendo os dados, mas não os invariantes.

O problema de proteger invariantes se apresenta mesmo em aplicativos de thread único e não tem nada em comum com multi-threading e mutexes.

Além disso, se você precisar proteger invariantes, ainda poderá usar o semáforo binário que nunca é recursivo.


fonte
Verdade. Existem melhores mecanismos para proteger um invariante.
ActiveTrayPrntrTagDataStrDrvr
8
Este deve ser um comentário para a resposta que ofereceu essa afirmação. Os mutexes não apenas protegem os dados, mas também os invariantes. Tente escrever algum contêiner simples (o mais simples é uma pilha) em termos de atômica (onde os dados se protegem) em vez de mutexes e você entenderá a declaração.
David Rodríguez - dribeas
Os mutexes não protegem dados, eles protegem um invariante. Essa invariante pode ser usada para proteger os dados.
Jon Hanna
4

Um dos principais motivos pelos quais mutexes recursivos são úteis é no caso de acessar os métodos várias vezes pelo mesmo encadeamento. Por exemplo, digamos que, se o bloqueio mutex estiver protegendo um banco A / c para retirada, se houver uma taxa também associada a esse saque, o mesmo mutex deverá ser usado.

avis
fonte
4

O único bom caso de uso para recursão mutex é quando um objeto contém vários métodos. Quando qualquer um dos métodos modifica o conteúdo do objeto e, portanto, deve bloquear o objeto antes que o estado seja consistente novamente.

Se os métodos usarem outros métodos (por exemplo: addNewArray () chama addNewPoint () e finaliza com recheckBounds ()), mas qualquer uma dessas funções precisa bloquear o mutex, o mutex recursivo é um ganha-ganha.

Para qualquer outro caso (resolver apenas códigos ruins, usá-los mesmo em objetos diferentes) está claramente errado!

DarkZeros
fonte
1

Para que servem os mutexes não recursivos?

Eles são absolutamente bons quando você precisa garantir que o mutex esteja desbloqueado antes de fazer algo. Isso ocorre porque pthread_mutex_unlockpode garantir que o mutex seja desbloqueado apenas se não for recursivo.

pthread_mutex_t      g_mutex;

void foo()
{
    pthread_mutex_lock(&g_mutex);
    // Do something.
    pthread_mutex_unlock(&g_mutex);

    bar();
}

Se g_mutexnão for recursivo, o código acima é garantido para ligar bar()com o mutex desbloqueado .

Assim, eliminar a possibilidade de um impasse no caso bar()de ser uma função externa desconhecida que pode muito bem fazer algo que pode resultar em outro encadeamento tentando adquirir o mesmo mutex. Tais cenários não são incomuns em aplicativos criados em conjuntos de encadeamentos e em aplicativos distribuídos, nos quais uma chamada entre processos pode gerar um novo encadeamento sem que o programador cliente perceba isso. Em todos esses cenários, é melhor chamar as funções externas mencionadas somente após o bloqueio ser liberado.

Se g_mutexfosse recursivo, simplesmente não haveria maneira de garantir que ele estivesse desbloqueado antes de fazer uma ligação.

Igor G
fonte