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.
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.
fonte
Semaphore
,.A::foo()
ainda pode ter deixado o objeto em um estado inconsistente antes de chamarA::bar()
. O que mutex, recursivo ou não, tem algo a ver com este caso?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. "
fonte
...you're not DONE until they're [recursive mutex] all gone.. Or sit back and let someone else do the design.
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
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.
fonte
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!
fonte
Eles são absolutamente bons quando você precisa garantir que o mutex esteja desbloqueado antes de fazer algo. Isso ocorre porque
pthread_mutex_unlock
pode garantir que o mutex seja desbloqueado apenas se não for recursivo.Se
g_mutex
não for recursivo, o código acima é garantido para ligarbar()
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_mutex
fosse recursivo, simplesmente não haveria maneira de garantir que ele estivesse desbloqueado antes de fazer uma ligação.fonte