c++ - list:: empty() comportamiento de subprocesos mĂșltiples?
multithreading optimization (3)
Está bien si la llamada a la
list::empty()
no es correcta el 100% del tiempo.
No, no esta bien. Si verifica si la lista está vacía fuera de algún mecanismo de sincronización (bloqueando el mutex), entonces tiene una carrera de datos. Tener una carrera de datos significa que tienes un comportamiento indefinido. Tener un comportamiento indefinido significa que ya no podemos razonar sobre el programa y cualquier resultado que obtenga es "correcto".
Si valora su cordura, tomará el golpe de rendimiento y bloqueará el mutex antes de verificar. Dicho esto, es posible que la lista ni siquiera sea el contenedor correcto para usted. Si puede hacernos saber exactamente qué está haciendo con él, podríamos sugerirle un mejor contenedor.
Tengo una lista de la que quiero diferentes hilos para tomar elementos.
Para evitar bloquear el mutex que protege la lista cuando está vacío, verifico
empty()
antes de bloquear.
Está bien si la llamada a la
list::empty()
no es correcta el 100% del tiempo.
Solo quiero evitar fallar o interrumpir las llamadas concurrentes
list::push()
y
list::pop()
.
¿Estoy seguro de suponer que VC ++ y Gnu GCC solo a veces se
empty()
mal y nada peor?
if(list.empty() == false){ // unprotected by mutex, okay if incorrect sometimes
mutex.lock();
if(list.empty() == false){ // check again while locked to be certain
element = list.back();
list.pop_back();
}
mutex.unlock();
}
Como ejemplo de cómo las cosas podrían salir mal:
Un compilador lo suficientemente inteligente podría ver que
mutex.lock()
no puede cambiar el valor de retorno
list.empty()
y, por lo tanto, omitir por completo la comprobación
if
interna, lo que finalmente lleva a un
pop_back
en una lista que eliminó su último elemento después del primer
if
.
¿Por qué puede hacer eso?
No hay sincronización en
list.empty()
, por lo tanto, si se cambiara simultáneamente, constituiría una carrera de datos.
El estándar dice que los programas no tendrán carreras de datos, por lo que el compilador lo dará por sentado (de lo contrario, casi no podría realizar ninguna optimización).
Por lo tanto, puede asumir una perspectiva de subproceso único en la lista no sincronizada.empty
list.empty()
y concluir que debe permanecer constante.
Esta es solo una de varias optimizaciones (o comportamientos de hardware) que podrían romper su código.
Hay una lectura y una escritura (muy probablemente para el miembro de
size
de
std::list
, si suponemos que se llama así) que no están sincronizadas
entre sí
.
Imagine que un hilo llama a
empty()
(en su
if()
externo) mientras que el otro hilo ingresa al
if()
interno y ejecuta
pop_back()
.
Entonces está leyendo una variable que posiblemente se está modificando.
Este es un comportamiento indefinido.