Bloqueio Recursivo (Mutex) vs Bloqueio Não Recursivo (Mutex)

POSIX permite que mutexes sejam recursivas. Isso significa que o mesmo thread pode bloquear o mesmo mutex duas vezes e não ficará bloqueado. Claro que também precisa desbloqueá-lo duas vezes, caso contrário, nenhum outro segmento pode obter o mutex. Nem todos os sistemas que suportam pthreads também suportam mutexes recursivas, mas se eles querem estar em conformidade com POSIX, eles precisam .

Outras APIs (mais APIs de alto nível) também costumam oferecer mutexes, geralmente chamados de Locks. Alguns sistemas / linguagens (por exemplo, Cocoa Objective-C) oferecem mutexes recursivos e não recursivos. Algumas línguas também oferecem apenas uma ou outra. Por exemplo, em mutexes Java são sempre recursivas (o mesmo thread pode “sincronizar” duas vezes no mesmo object). Dependendo de qual outra funcionalidade de thread eles oferecem, não ter mutex recursivas pode não ser problema, já que elas podem ser facilmente escritas (eu já implementei mutex recursivas eu mesmo com base em operações mutex / condition mais simples).

O que eu realmente não entendo: O que são mutexes não-recursivos bons para? Por que eu iria querer ter um impasse de thread se ele bloqueia o mesmo mutex duas vezes? Mesmo as linguagens de alto nível que poderiam evitar isso (por exemplo, testar se isso trava e lançar uma exceção, se isso acontecer) normalmente não fazem isso. Eles vão deixar o impasse thread em vez disso.

Isso é apenas para casos, onde eu acidentalmente bloqueio duas vezes e só desbloqueio uma vez e no caso de um mutex recursivo, seria mais difícil encontrar o problema, então em vez disso eu tenho deadlock imediatamente para ver onde aparece o bloqueio incorreto? Mas eu não poderia fazer o mesmo com um contador de bloqueio retornado quando desbloqueado e em uma situação, onde tenho certeza que lancei o último bloqueio e o contador não é zero, posso lançar uma exceção ou registrar o problema? Ou existe algum outro caso de uso mais útil de mutexes não recursivas que eu não vejo? Ou talvez seja apenas performance, já que um mutex não-recursivo pode ser um pouco mais rápido que um recursivo? No entanto, eu testei isso e a diferença não é tão grande assim.

A diferença entre um mutex recursivo e não recursivo tem a ver com a propriedade. No caso de um mutex recursivo, o kernel tem que acompanhar o segmento que realmente obteve o mutex na primeira vez, para que ele possa detectar a diferença entre a recursion versus um thread diferente que deveria ser bloqueado. Como outra resposta apontou, há uma questão da sobrecarga adicional disso tanto em termos de memory para armazenar esse contexto como também nos ciclos necessários para mantê-lo.

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

Como o mutex recursivo tem um senso de propriedade, o thread que captura o mutex deve ser o mesmo thread que libera o mutex. No caso de exclusões mutuais não-recursivas, não há senso de propriedade e qualquer encadeamento pode liberar o mutex, não importando qual encadeamento originalmente levou 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 o usa como dispositivo de synchronization ou sinalização entre dois ou mais encadeamentos.

Outra propriedade que vem com um senso de propriedade em um mutex é a capacidade de suportar a inheritance prioritária. Como o kernel pode rastrear o thread que possui o mutex e também a identidade de todos os bloqueadores, em um sistema thread segmentado, torna-se possível escalar a prioridade do thread que atualmente possui o mutex para a prioridade do thread de prioridade mais alta. que atualmente está bloqueando o mutex. Essa inheritance impede o problema de inversão de prioridade que pode ocorrer nesses casos. (Note que nem todos os sistemas suportam inheritance de prioridade em tais mutexes, mas é outra característica 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 recursion e, opcionalmente, inheritance de prioridade
  • semáforo binário – sem recursion, sem inheritance, exclusão simples, tomador e doador não precisa ser o mesmo thread, disponível para transmissão
  • semáforo de contagem – sem recursion ou inheritance, atua como um contador de recurso coerente de qualquer contagem inicial desejada, bloco somente threads onde a contagem de líquidos contra o recurso é zero.

Mais uma vez, isso varia um pouco por plataforma – especialmente o que eles chamam essas coisas, mas isso deve ser representativo dos conceitos e vários mecanismos em jogo.

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

Exemplo: A :: foo () adquire o bloqueio. Em seguida, chama B :: bar (). Isso funcionou bem quando você 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 recursivas, isso trava. Se você os tiver, corre, mas pode quebrar. A :: foo () pode ter deixado o object em um estado inconsistente antes de chamar bar (), assumindo que baz () não pode 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 é a razão por que ambos os methods adquiriram o bloqueio.

O modelo mental correto 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. As fechaduras de reinput são perigosas porque, da segunda vez que você adquire a fechadura, você não pode ter mais certeza de que a invariável é verdadeira.

Se você está feliz com bloqueios reentrantes, é só porque você não teve que depurar um problema como este antes. Java tem bloqueios não reentrantes atualmente em java.util.concurrent.locks, a propósito.

Conforme escrito pelo próprio Dave Butenhof :

“O maior de todos os grandes problemas com exclusões recursivas é que eles encorajam você a perder completamente o controle do seu esquema de bloqueio e escopo. Isso é mortal. Mal. É o” comedor de threads “. Você segura bloqueios pelo menor tempo possível. Período: Sempre, se você está chamando algo com um cadeado, simplesmente porque você não sabe que está preso, ou porque você não sabe se o alvo precisa do mutex, então você está segurando isso por muito tempo. apontando uma espingarda para a sua aplicação e puxando o gatilho. Você presumivelmente começou a usar threads para obter a simultaneidade, mas você apenas evitou a concorrência. ”

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

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

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

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

Um motivo principal pelo qual mutexes recursivas são úteis é no caso de acessar os methods várias vezes pelo mesmo thread. Por exemplo, digamos que se o bloqueio mutex estiver protegendo um banco A / c para retirada, então, se houver uma taxa também associada a essa retirada, o mesmo mutex deverá ser usado.

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

Se os methods usarem outros methods (isto é: addNewArray () chama addNewPoint () e finaliza com recheckBounds ()), mas qualquer uma dessas funções por si só precisa bloquear o mutex, então o mutex recursivo é um win-win.

Para qualquer outro caso (resolver apenas codificação incorreta, usá-lo mesmo em objects diferentes) está claramente errado!