Por que não consigo verificar se um mutex está bloqueado?

28

O C ++ 14 parece ter omitido um mecanismo para verificar se um std::mutexestá bloqueado ou não. Veja esta pergunta SO:

https://stackoverflow.com/questions/21892934/how-to-assert-if-a-stdmutex-is-locked

Existem várias maneiras de contornar isso, por exemplo, usando;

std::mutex::try_lock()
std::unique_lock::owns_lock()

Mas nenhuma dessas soluções é particularmente satisfatória.

try_lock()tem permissão para retornar um falso negativo e tem um comportamento indefinido se o encadeamento atual bloqueou o mutex. Também tem efeitos colaterais. owns_lock()requer a construção de um unique_lockem cima do original std::mutex.

Obviamente, eu poderia fazer o meu próprio, mas prefiro entender as motivações para a interface atual.

A capacidade de verificar o status de um mutex (por exemplo std::mutex::is_locked()) não parece uma solicitação esotérica para mim, então suspeito que o Comitê Padrão tenha omitido deliberadamente esse recurso em vez de ser uma supervisão.

Por quê?

Edit: Ok, então talvez este caso de uso não seja tão comum quanto eu esperava, então ilustrarei meu cenário específico. Eu tenho um algoritmo de aprendizado de máquina que é distribuído em vários threads. Cada encadeamento opera de forma assíncrona e retorna para um pool principal depois de concluir um problema de otimização.

Em seguida, bloqueia um mutex mestre. O encadeamento deve, então, escolher um novo pai para modificar uma prole, mas só pode escolher entre os pais que atualmente não têm filhos que estão sendo otimizados por outros encadeamentos. Portanto, preciso fazer uma pesquisa para encontrar pais que não estão bloqueados no momento por outro segmento. Não há risco de o status do mutex ser alterado durante a pesquisa, pois o mutex do encadeamento principal está bloqueado. Obviamente, existem outras soluções (atualmente estou usando um sinalizador booleano), mas achei que o mutex oferece uma solução lógica para esse problema, pois existe para fins de sincronização entre threads.

quant
fonte
42
Você não pode realmente verificar razoavelmente se um mutex está bloqueado, porque um nanossegundo após a verificação pode ser desbloqueado ou bloqueado. Portanto, se você escreveu "if (mutex_is_locked ()) ...", então mutex_is_locked pode retornar o resultado correto, mas quando o "if" for executado, ele estará errado.
precisa saber é o seguinte
11
Este ^. Que informações úteis você espera obter is_locked?
11136 Useless
3
Isso parece um problema XY. Por que você está tentando impedir a reutilização dos pais apenas enquanto um filho está sendo gerado? Você tem um requisito para que qualquer pai ou mãe tenha apenas exatamente um filho? Seu bloqueio não impedirá isso. Você não tem gerações claras? Caso contrário, você está ciente de que indivíduos que podem ser otimizados mais rapidamente têm maior condicionamento físico, pois podem ser selecionados com mais frequência / mais cedo? Se você usa gerações, por que não seleciona todos os pais com antecedência e deixa que os threads recuperem os pais de uma fila? A geração de filhos é realmente tão cara que você precisa de vários threads?
amon
10
@quant - Não vejo por que o objeto pai mutexes no seu aplicativo de exemplo precisa ser mutexes: se você tem um mutex mestre que está bloqueado sempre que definido, basta usar uma variável booleana para indicar seu status.
Periata Breatta 11/09/16
4
Não concordo com a última frase da pergunta. Um valor booleano simples é muito mais limpo que um mutex aqui. Torne-o um bool atômico se você não quiser bloquear o mutex principal por "retornar" um pai.
Sebastian Redl

Respostas:

53

Eu posso ver pelo menos dois problemas graves com a operação sugerida.

O primeiro já foi mencionado em um comentário por @ gnasher729 :

Você não pode realmente verificar razoavelmente se um mutex está bloqueado, porque um nanossegundo após a verificação pode ser desbloqueado ou bloqueado. Portanto, se você escreveu if (mutex_is_locked ()) …, mutex_is_lockedpode retornar o resultado correto, mas, quando ifé executado, ele está errado.

A única maneira de garantir que a propriedade "está bloqueada no momento" de um mutex não seja alterada é: bem, bloqueie-a você mesmo.

O segundo problema que vejo é que, a menos que você bloqueie um mutex, seu thread não será sincronizado com o thread que havia bloqueado o mutex anteriormente. Portanto, nem sequer está bem definido falar sobre “antes” e “depois” e se o mutex está bloqueado ou não, é como perguntar se o gato de Schrödiger está vivo no momento sem tentar abrir a caixa.

Se eu entendi corretamente, os dois problemas seriam discutíveis em seu caso particular, graças ao bloqueio do mestre mutex. Mas esse não me parece um caso particularmente comum, por isso acho que o comitê fez a coisa certa ao não adicionar uma função que possa ser útil em cenários muito especiais e causar danos a todos os outros. (No espírito de: “Tornar as interfaces fáceis de usar corretamente e difíceis de usar incorretamente.”)

E se eu puder dizer, acho que a configuração que você possui atualmente não é a mais elegante e pode ser refatorada para evitar o problema completamente. Por exemplo, em vez de o thread mestre verificar todos os pais em potencial por um que não esteja bloqueado no momento, por que não manter uma fila de pais prontos? Se um encadeamento deseja otimizar outro, ele abre o próximo da fila e, assim que tiver novos pais, os adiciona à fila. Dessa forma, você nem precisa do thread principal como coordenador.

5gon12eder
fonte
Obrigado, esta é uma boa resposta. O motivo de eu não querer manter uma fila de pais prontos é que preciso preservar a ordem em que os pais foram criados (pois isso dita sua vida útil). Isso é feito facilmente com uma fila LIFO. Se eu começar a puxar as coisas para dentro e para fora, teria que manter um mecanismo de pedidos separado que complicaria as coisas, daí a abordagem atual.
quant
14
@quant: Se você tem dois propósitos para enfileirar os pais, pode fazê-lo com duas filas ....
@quant: você está excluindo um item (no máximo) uma vez, mas presumivelmente fazendo o processamento em várias vezes, para otimizar o caso raro em detrimento do caso comum. Isso raramente é desejável.
Jerry Coffin
2
Mas é razoável perguntar se o segmento atual bloqueou o mutex.
Expiação limitada
@LimitedAtonement Na verdade não. Para fazer isso, o mutex precisa armazenar informações adicionais (ID do thread), tornando-o mais lento. Os mutexes recursivos já fazem isso, você deve fazê-los.
StaceyGirl
9

Parece que você está usando os mutexes secundários para não bloquear o acesso a um problema de otimização, mas para determinar se um problema de otimização está sendo otimizado agora ou não.

Isso é completamente desnecessário. Eu teria uma lista de problemas que precisam ser otimizados, uma lista de problemas que estão sendo otimizados agora e uma lista de problemas que foram otimizados. (Não considere "lista" literalmente, considere "qualquer estrutura de dados apropriada).

As operações de adicionar um novo problema à lista de problemas não otimizados ou de mover um problema de uma lista para a seguinte seriam realizadas sob a proteção do único mutex "mestre".

gnasher729
fonte
11
Você não acha que um objeto do tipo std::mutexé apropriado para essa estrutura de dados?
quant
2
@quant - não. std::mutexdepende de uma implementação mutex definida pelo sistema operacional que pode levar recursos (por exemplo, identificadores) limitados e lentos para alocar e / ou operar. O uso de um único mutex para bloquear o acesso a uma estrutura de dados interna provavelmente será muito mais eficiente e possivelmente mais escalável.
Periata Breatta
11
Considere também Variáveis ​​de condição. Eles podem facilitar muito a estrutura de dados como essa.
Cort Ammon - Restabelece Monica
2

Como outros disseram, não há nenhum caso de uso is_lockedem que um mutex tenha algum benefício, é por isso que a função não existe.

O caso com o qual você está tendo problemas é incrivelmente comum, é basicamente o que os threads de trabalho fazem, que são um dos, se não implementações de threads a mais comum.

Você tem uma prateleira com 10 caixas. Você tem 4 trabalhadores trabalhando com essas caixas. Como você garante que os quatro trabalhadores trabalhem em caixas diferentes? O primeiro trabalhador tira uma caixa da prateleira antes de começar a trabalhar nela. O segundo trabalhador vê 9 caixas na prateleira.

Não há mutexes para bloquear as caixas; portanto, não é necessário ver o estado do mutex imaginário na caixa e abusar de um mutex como um booleano está errado. O mutex trava a prateleira.

Pedro
fonte
1

Além das duas razões apresentadas na resposta de 5gon12eder acima, gostaria de acrescentar que não é necessário nem desejável.

Se você já está segurando um mutex, é melhor saber que está segurando! Você não precisa perguntar. Assim como possuir um bloco de memória ou qualquer outro recurso, você deve saber exatamente se o possui ou não e quando é apropriado liberar / excluir o recurso.
Se não for esse o caso, seu programa foi mal projetado e você está enfrentando problemas.

Se você precisar acessar o recurso compartilhado protegido pelo mutex, e ainda não o tiver, precisará adquiri-lo. Não há outra opção, caso contrário, a lógica do seu programa não está correta.
Você pode achar que o bloqueio é aceitável ou inaceitável, em ambos os casos, lock()ou try_lock()dará o comportamento desejado. Tudo o que você precisa saber, de maneira positiva e sem dúvida, é se você adquiriu o mutex com sucesso (o valor de retorno try_lockindica). É irrelevante se alguém o segura ou se você tem um fracasso falso.

Em todos os outros casos, sem rodeios, não é da sua conta. Você não precisa saber e não deve fazer suposições (para os problemas de pontualidade e sincronização mencionados na outra pergunta).

Damon
fonte
11
E se eu quiser executar uma operação de classificação nos recursos atualmente disponíveis para bloqueio?
quant
Mas isso é algo realista para acontecer? Eu consideraria isso bastante incomum. Eu diria que os recursos já têm algum tipo intrínseco de classificação, então você precisará fazer (adquirir o bloqueio para) o mais importante primeiro. Exemplo: é necessário atualizar a simulação física antes da renderização. Ou, como a classificação é mais ou menos deliberada, você também pode try_lockusar o primeiro recurso e, se esse falhar, tentar o segundo. Exemplo: Três conexões persistentes em pool com o servidor de banco de dados e você precisa usar uma para enviar um comando.
Damon
4
@quant - "uma operação de classificação dos recursos atualmente disponíveis para bloqueio" - em geral, fazer esse tipo de coisa é uma maneira muito fácil e rápida de escrever código que trava uma maneira que você luta para descobrir. Tornar determinística a aquisição e liberação de bloqueios é, em quase todos os casos, a melhor política. Procurar um bloqueio com base em um critério que possa mudar está causando problemas.
Periata Breatta 11/09/16
@PeriataBreatta Meu programa é intencionalmente indeterminado. Vejo agora que esse atributo não é comum, para que eu possa entender a omissão de recursos como is_locked()esse que possam facilitar esse comportamento.
quant
A classificação e o bloqueio @quant são problemas totalmente separados. Se você deseja classificar ou reordenar uma fila com um bloqueio, bloqueie-o, classifique-o e desbloqueie-o. Se você precisar is_locked, existe uma solução muito melhor para o seu problema do que a que você tem em mente.
Peter Peter
1

Você pode querer usar atomic_flag com a ordem de memória padrão. Ele não possui corridas de dados e nunca lança exceções, como o mutex faz com várias chamadas de desbloqueio (e aborta incontrolavelmente, devo acrescentar ...). Como alternativa, existe atômico (por exemplo, atômico [bool] ou atômico [int] (com colchetes triangulares, não [])), que possui boas funções como load e compare_exchange_strong.

Andrew
fonte
1

Quero adicionar um caso de uso para isso: isso permitiria que uma função interna assegurasse como pré-condição / asserção que o chamador está de fato segurando a trava.

Para classes com várias funções internas, e possivelmente muitas funções públicas chamando-as, isso poderia garantir que alguém adicionando outra função pública chamando a interna realmente adquirisse o bloqueio.

class SynchronizedClass
{

public:

void publicFunc()
{
  std::lock_guard<std::mutex>(_mutex);

  internalFuncA();
}

// A lot of code

void newPublicFunc()
{
  internalFuncA(); // whops, forgot to acquire the lock
}


private:

void internalFuncA()
{
  assert(_mutex.is_locked_by_this_thread());

  doStuffWithLockedResource();
}

};
B3ret
fonte