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

(Nota: Gran parte de esto es redundante con comentarios enCarga masiva de la CPU usando std :: lock (c ++ 11), pero creo que este tema merece sus propias preguntas y respuestas.)

Recientemente me encontré con un código de ejemplo de C ++ 11 que se parecía a esto:

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);

Wow, pensé,std::lock suena interesante. Me pregunto qué dice la norma que hace.

C ++ 11 sección 30.4.3 [thread.lock.algorithm], párrafos (4) y (5):

bloqueo de vaciado de plantilla (L1 &, L2 &, L3 & ...);

4 Requiere: Cada tipo de parámetro de plantilla deberá cumplir con los requisitos de bloqueo, [Nota:unique_lock La plantilla de clase cumple con estos requisitos cuando se crea una instancia adecuada. - nota final]

5 Efectos: Todos los argumentos están bloqueados a través de una secuencia de llamadas alock(), try_lock()ounlock() en cada argumento. La secuencia de llamadas no debe producir un interbloqueo, pero por lo demás no se especifica. [Nota: se debe utilizar un algoritmo de evitación de interbloqueo, como el intento y retroceso, pero el algoritmo específico no se especifica para evitar las implementaciones de restricción excesiva. - nota final] Si una llamada alock() otry_lock() arroja una excepción,unlock() será llamado para cualquier argumento que haya sido bloqueado por una llamada alock() otry_lock().

Considere el siguiente ejemplo. Llámalo "Ejemplo 1":

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

¿Puede este punto muerto?

Una simple lectura de la norma dice "no". ¡Genial! Tal vez el compilador pueda ordenar mis bloqueos para mí, lo que sería un poco limpio.

Ahora prueba el ejemplo 2:

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

¿Puede este punto muerto?

Una vez más, una simple lectura de la norma dice "no". UH oh. La única forma de hacerlo es con algún tipo de bucle de retroceso y reintento. Más sobre eso a continuación.

Finalmente, Ejemplo 3:

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

¿Puede este punto muerto?

Una vez más, una simple lectura de la norma dice "no". (Si la "secuencia de llamadas alock()"en una de estas invocaciones no está" resultando en un punto muerto ", ¿qué es, exactamente?) Sin embargo, estoy bastante seguro de que esto no se puede implementar, así que supongo que no es lo que significaron.

Esto parece ser una de las peores cosas que he visto en un estándar de C ++. Supongo que comenzó como una idea interesante: dejar que el compilador asigne un orden de bloqueo. Pero una vez que el comité lo ha masticado, el resultado es impensable o requiere un ciclo de reintento. Y sí, es una mala idea.

Se puede argumentar que "retroceder y reintentar" a veces es útil. Eso es cierto, pero solo cuando no sabes qué cerraduras intentas agarrar por adelantado. Por ejemplo, si la identidad del segundo bloqueo depende de los datos protegidos por el primero (por ejemplo, porque está atravesando alguna jerarquía), entonces es posible que tenga que hacer un giro giratorio. Pero en ese caso no puedes usar este gadget, porque no conoces todos los bloqueos por adelantado. Por otro lado, sihacer saber qué bloqueos desea por adelantado, entonces (casi) siempre desea simplemente imponer un pedido, no realizar un bucle.

Además, tenga en cuenta que el Ejemplo 1 puede bloquearse en tiempo real si la implementación simplemente agarra los bloqueos en orden, retrocede y vuelve a intentar.

En resumen, este dispositivo me parece, en el mejor de los casos, inútil. Sólo una mala idea por todos lados.

OK, preguntas. (1) ¿Alguna de mis afirmaciones o interpretaciones es incorrecta? (2) Si no, ¿qué diablos estaban pensando? (3) ¿Deberíamos todos estar de acuerdo en que la "mejor práctica" es evitarstd::lock ¿completamente?

[Actualizar]

Algunas respuestas dicen que estoy malinterpretando el estándar, luego continúo interpretándolo de la misma manera que lo hice, y luego confundí la especificación con la implementación.

Así que, para ser claros:

En mi lectura de la norma, el Ejemplo 1 y el Ejemplo 2 no pueden interrumpirse. El ejemplo 3 puede, pero solo porque evitar el interbloqueo en ese caso no es implementable.

El punto central de mi pregunta es que para evitar el interbloqueo en el Ejemplo 2 se requiere un ciclo de retroceso y reintento, y estos ciclos son una práctica extremadamente mala. (Sí, algún tipo de análisis estático en este ejemplo trivial podría hacerlo evitable, pero no en el caso general). También tenga en cuenta que GCC implementa esto como un bucle ocupado.

[Actualización 2]

Creo que mucha de la desconexión aquí es una diferencia básica en la filosofía.

Hay dos enfoques para escribir software, especialmente software multihilo.

En un enfoque, juntan un montón de cosas y lo ejecutan para ver qué tan bien funciona. Nunca está convencido de que su código tenga un problema a menos que alguien pueda demostrarlo en un sistema real, ahora mismo, hoy.

En el otro enfoque, usted escribe código que puede analizarse rigurosamente para probar que no tiene carreras de datos, que todos sus bucles terminan con la probabilidad 1, y así sucesivamente. Usted realiza este análisis estrictamente dentro del modelo de máquina garantizado por la especificación de idioma, no en ninguna implementación en particular.

Los defensores de este último enfoque no están impresionados por ninguna demostración en CPU, compiladores, versiones menores de compiladores, sistemas operativos, tiempos de ejecución, etc. Dichas demostraciones son apenas interesantes y totalmente irrelevantes. Si tualgoritmo tiene una carrera de datos, está roto, no importa lo que pase cuando lo ejecutas. Si tualgoritmo tiene un bloqueo vital, está roto, no importa lo que pase cuando lo ejecutas. Etcétera.

En mi mundo, el segundo enfoque se llama "Ingeniería". No estoy seguro de cómo se llama el primer enfoque.

Por lo que puedo decir, lastd::lock La interfaz es inútil para la ingeniería. Me encantaría estar equivocado.

Respuestas a la pregunta(4)

Su respuesta a la pregunta