Is std::lock() ill-defined, unimplementable, or useless?

(Nota: Muito disso é redundante com comentários sobreCarga de CPU massiva usando std :: lock (c ++ 11), mas acho que esse tópico merece suas próprias perguntas e respostas.)

Recentemente, encontrei um exemplo de código C ++ 11 que se parecia com algo assim:

std::unique_lock<std::mutex> lock1(from_acct.mutex, std::defer_lock);
std::unique_lock<std::mutex> lock2(to_acct.mutex, std::defer_lock);
std::lock(lock1, lock2); // avoid deadlock
transfer_money(from_acct, to_acct, amount);

Uau, eu penseistd::lock Soa interessante. Eu me pergunto o que a norma diz que faz?

C ++ 11 seção 30.4.3 [thread.lock.algorithm], parágrafos (4) e (5):

bloqueio de vazio de modelo (L1 &, L2 &, L3 & ...);

4 Requer: Cada tipo de parâmetro modelo deve atender aos requisitos Lockable, [Nota: Ounique_lock modelo de classe atende a esses requisitos quando devidamente instanciado. - nota final

5 Efeitos: Todos os argumentos são bloqueados por meio de uma sequência de chamadas paralock(), try_lock()ouunlock() em cada argumento. A sequência de chamadas não resultará em deadlock, mas será de outra forma não especificada. [Nota: Um algoritmo de evasão de conflito como try-and-back-off deve ser usado, mas o algoritmo específico não é especificado para evitar implementações com restrições excessivas. - nota final] Se uma chamada paralock() outry_lock() lança uma exceção,unlock() deve ser chamado para qualquer argumento que tenha sido bloqueado por uma chamada paralock() outry_lock().

Considere o seguinte exemplo. Chame de "Exemplo 1":

Thread 1                    Thread 2
std::lock(lock1, lock2);    std::lock(lock2, lock1);

Pode este impasse?

Uma leitura simples do padrão diz "não". Ótimo! Talvez o compilador possa ordenar minhas fechaduras para mim, o que seria legal.

Agora tente o Exemplo 2:

Thread 1                                  Thread 2
std::lock(lock1, lock2, lock3, lock4);    std::lock(lock3, lock4);
                                          std::lock(lock1, lock2);

Pode este impasse?

Aqui, novamente, uma leitura simples do padrão diz "não". Uh oh. A única maneira de fazer isso é com algum tipo de loop back-off-and-retry. Mais sobre isso abaixo.

Finalmente, Exemplo 3:

Thread 1                          Thread 2
std::lock(lock1,lock2);           std::lock(lock3,lock4);
std::lock(lock3,lock4);           std::lock(lock1,lock2);

Pode este impasse?

Mais uma vez, uma leitura simples do padrão diz "não". (Se a "sequência de chamadas paralock()"em uma dessas invocações não é" resultando em deadlock ", o que é, exatamente?) No entanto, tenho certeza que isso não é implementável, então eu suponho que não é o que eles significam.

Esta parece ser uma das piores coisas que eu já vi em um padrão C ++. Eu estou supondo que começou como uma idéia interessante: Deixe o compilador atribuir uma ordem de bloqueio. Mas uma vez que o comitê o mastigou, o resultado não é implementável ou requer um loop de repetição. E sim, isso é uma má ideia.

Você pode argumentar que "recuar e tentar novamente" às ​​vezes é útil. Isso é verdade, mas apenas quando você não sabe quais bloqueios você está tentando pegar na frente. Por exemplo, se a identidade da segunda trava depender de dados protegidos pela primeira (digamos, porque você está percorrendo alguma hierarquia), então você pode ter que fazer um spin-grab-release-grab. Mas, nesse caso, você não pode usar este gadget, porque você não conhece todos os bloqueios na frente. Por outro lado, se vocêFaz saber quais bloqueios você quer na frente, então você (quase) sempre quer simplesmente impor uma ordenação, não fazer um loop.

Além disso, observe que o Exemplo 1 pode bloquear a transmissão se a implementação simplesmente pegar os bloqueios em ordem, recuar e tentar novamente.

Em suma, este gadget me parece inútil na melhor das hipóteses. Apenas uma má ideia ao redor.

OK, perguntas (1) Alguma das minhas afirmações ou interpretações está errada? (2) Se não, o que diabos eles estavam pensando? (3) Devemos todos concordar que a "melhor prática" é evitarstd::lock completamente?

[Atualizar]

Algumas respostas dizem que estou interpretando mal o padrão e, em seguida, passo a interpretá-lo da mesma maneira que fiz, confundindo a especificação com a implementação.

Então, só para ficar claro:

Na minha leitura do padrão, o Exemplo 1 e o Exemplo 2 não podem bloquear. O Exemplo 3 pode, mas apenas porque evitar o deadlock nesse caso não é implementável.

O ponto principal da minha pergunta é que evitar o deadlock para o Exemplo 2 requer um loop de recuo e repetição, e esses loops são uma prática extremamente ruim. (Sim, algum tipo de análise estática neste exemplo trivial poderia tornar isso evitável, mas não no caso geral.) Observe também que o GCC implementa essa coisa como um loop ocupado.

[Atualização 2]

Eu acho que muito da desconexão aqui é uma diferença básica na filosofia.

Existem duas abordagens para escrever software, especialmente software multi-threaded.

Em uma abordagem, você joga um monte de coisas e corre para ver como funciona. Você nunca está convencido de que seu código tem um problema, a menos que alguém possa demonstrar esse problema em um sistema real, hoje mesmo.

Na outra abordagem, você escreve um código que pode ser rigorosamente analisado para provar que não tem corridas de dados, que todos os seus loops terminam com probabilidade 1 e assim por diante. Você executa essa análise estritamente dentro do modelo de máquina garantido pela especificação de idioma, não em qualquer implementação específica.

Os defensores da última abordagem não ficam impressionados com quaisquer demonstrações em CPUs, compiladores, versões menores do compilador, sistemas operacionais, tempos de execução específicos, etc. Tais demonstrações são pouco interessantes e totalmente irrelevantes. Se seualgoritmo tem uma corrida de dados, ela está quebrada, não importa o que aconteça quando você a executar. Se seualgoritmo tem um livelock, está quebrado, não importa o que aconteça quando você o executar. E assim por diante.

No meu mundo, a segunda abordagem é chamada de "Engenharia". Não sei ao certo qual é a primeira abordagem.

Tanto quanto eu posso dizer, ostd::lock interface é inútil para engenharia. Eu adoraria ser provado errado.

questionAnswers(4)

yourAnswerToTheQuestion