2010-05-01 14 views
11

Así que soy consciente de que nada es atómico en C++. Pero estoy tratando de averiguar si hay suposiciones "pseudoatómicas" que pueda hacer. La razón es que quiero evitar usar mutexes en algunas situaciones simples en las que solo necesito garantías muy débiles.operaciones "pseudoatómicas" en C++

1) Supongamos que he definido globalmente volatile bool b, que inicialmente establecí como verdadero. A continuación, ejecuto un hilo que ejecuta un ciclo

while(b) doSomething(); 

Mientras tanto, en otro hilo, ejecuto b = verdadero.

¿Puedo asumir que el primer hilo continuará ejecutándose? En otras palabras, si b comienza como verdadero, y el primer hilo verifica el valor de b al mismo tiempo que el segundo hilo asigna b = verdadero, ¿puedo suponer que el primer hilo leerá el valor de b como verdadero? ¿O es posible que en algún punto intermedio de la asignación b = verdadero, el valor de b se lea como falso?

2) Ahora supongamos que b es inicialmente falso. A continuación, el primer subproceso se ejecuta

bool b1=b; 
bool b2=b; 
if(b1 && !b2) bad(); 

mientras que el segundo subproceso ejecuta b = true. ¿Puedo suponer que bad() nunca se llama?

3) ¿Qué pasa con un int u otros tipos incorporados: supongamos que tengo volatile int i, que es inicialmente (digamos) 7, y luego asigno i = 7. ¿Puedo suponer que, en cualquier momento durante esta operación, desde cualquier hilo, el valor de i será igual a 7?

4) Tengo volatile int i = 7, y luego ejecuto i ++ desde algún subproceso, y todos los otros subprocesos solo leen el valor de i. ¿Puedo suponer que nunca tengo ningún valor en ningún hilo, excepto 7 o 8?

5) Tengo volatile int i, de un hilo ejecuto i = 7, y de otro ejecuto i = 8. Después, ¿tengo garantizado que sea 7 u 8 (o los dos valores que haya elegido asignar)?

+0

¿Dónde se lee que "nada es atómica en C++"? Tenía la impresión de que cualquier lectura o escritura de un solo byte era atómica. Entonces, ¿podría verificar el valor de un bool, o asignarle un char ... o tal vez eran 32 bits que se garantizaba que eran atómicos? ¿Sería entonces 64 en una arquitectura de 64 bits? – mpen

+1

@Mark: creo que quiso decir que el lenguaje C++ no garantiza la atomicidad de nada. Depende de la implementación específica (y la arquitectura de la CPU) lo que es o no es atómico. – jalf

+0

@jalf: bueno ... está bien, quizás no sea propiedad de C++, pero sigue siendo una suposición razonablemente segura. Por lo tanto, al menos podemos responder la pregunta (1) con certeza. – mpen

Respuesta

14

No hay subprocesos en C++ estándar y Threads cannot be implemented as a library.

Por lo tanto, la norma no tiene nada que decir sobre el comportamiento de los programas que usan subprocesos. Debe buscar las garantías adicionales proporcionadas por su implementación de subprocesamiento.

Dicho esto, en las implementaciones de roscado que he usado:

(1) sí, se puede asumir que los valores irrelevantes no se escriben en las variables. De lo contrario, el modelo de la memoria completa se va por la ventana. Pero tenga cuidado de que cuando diga "otro hilo" nunca establezca b en falso, eso quiere decir en cualquier lugar, siempre. Si lo hace, esa escritura podría ser reordenada para que ocurra durante su ciclo.

(2) no, el compilador puede reordenar las asignaciones a b1 y b2, por lo que es posible que b1 termine verdadero y b2 falso. En un caso tan simple, no sé por qué reordenaría, pero en casos más complejos podría haber muy buenas razones.

[Editar: Vaya, cuando llegué a responder (2) Había olvidado que b era volátil. Las lecturas de una variable volátil no se volverán a ordenar, lo siento, por lo que sí en una implementación de subprocesamiento típica (si existe tal cosa), puede suponer que no terminará con b1 verdadero y b2 falso.]

(3) igual que 1.volatile en general no tiene nada que ver con enhebrar en absoluto. Sin embargo, es bastante emocionante en algunas implementaciones (Windows), y podría implicar barreras de memoria.

(4) en una arquitectura donde las escrituras se int sí atómicas, aunque volatile no tiene nada que ver con ello. Consulte también ...

(5) revise los documentos con cuidado. Probablemente sí, y nuevamente volátil es irrelevante, porque en casi todas las arquitecturas int escribe son atómicas. Pero si int escribe no es atómico, entonces no (y no para la pregunta anterior), incluso si es volátil, en principio podría obtener un valor diferente. Sin embargo, dados esos valores 7 y 8, estamos hablando de una arquitectura bastante extraña para que el byte que contiene los bits relevantes se escriba en dos etapas, pero con valores diferentes, podría obtener una escritura parcial más plausible.

Para un ejemplo más plausible, supongamos que por alguna razón extraña tiene una int de 16 bits en una plataforma donde solo las escrituras de 8 bits son atómicas. Impar, pero legal, y dado que int debe tener al menos 16 bits, puede ver cómo podría suceder. Supongamos, además, que su valor inicial se incrementa 255. A continuación, legalmente podría implementarse como:

  • leer el valor antiguo
  • incremento en un registro
  • escribir el byte más significativo del resultado
  • escribir la Byte menos significativo del resultado.

A de sólo lectura hilo que interrumpe el hilo de incrementación entre la tercera y cuarta etapas de que, podría ver el valor 511. Si las escrituras están en el otro orden, se podría ver 0.

Un el valor inconsistente se puede dejar de forma permanente si un hilo está escribiendo 255, otro hilo está escribiendo al mismo tiempo 256, y las escrituras se entrelazan. Imposible en muchas arquitecturas, pero para saber que esto no sucederá, necesitas saber al menos algo sobre la arquitectura. Nada en el estándar de C++ lo prohíbe, porque el estándar de C++ se refiere a la interrupción de la ejecución por una señal, pero de lo contrario no tiene el concepto de ejecución interrumpida por otra parte del programa, y ​​ningún concepto de ejecución simultánea. Es por eso que los hilos no son solo otra biblioteca: agregar hilos fundamentalmente cambia el modelo de ejecución de C++. Requiere que la implementación haga las cosas de manera diferente, ya que eventualmente descubrirá si, por ejemplo, usa subprocesos bajo gcc y olvida especificar -pthreads.

Lo mismo podría suceder en una plataforma en la que alineadosint escrituras son atómicas, pero se les permite no alineados int escribe y no atómica. Por ejemplo, IIRC en x86, las escrituras no alineadas int no se garantizan atómicas si cruzan un límite de línea de caché. Los compiladores x86 no alinearán incorrectamente una variable declarada int, por este motivo y otros. Pero si juegas juegos con estructura de embalaje probablemente puedas provocar un ejemplo.

Así que: prácticamente cualquier implementación le dará las garantías que necesita, pero podría hacerlo de una manera bastante complicada.

En general, he encontrado que no vale la pena tratar de confiar en las garantías específicas de la plataforma sobre el acceso a la memoria, que no entiendo completamente, para evitar mutexes. Use un mutex y, si es demasiado lento, use una estructura sin cerraduras de alta calidad (o implemente un diseño para uno) escrita por alguien que realmente conozca la arquitectura y el compilador.Probablemente sea correcto, y sujeto a corrección probablemente superará cualquier cosa que me invente.

+1

§1.9 de la norma discute la máquina abstracta, que es como un hilo de ejecución. – Potatoswatter

+0

¿Por qué debería considerar cada hilo ejecutándose en una máquina abstracta separada de C++? ¿Su implementación de threading dice que esto es lo que sucede? ¿Es posible hacer esto? Si puedo considerar threadA como una máquina abstracta separada sujeta a las reglas de 1.9, entonces no hay forma legal de que una variable no volátil pueda cambiar el valor mientras estoy en ejecución, a menos que lo cambie. Y sin embargo, exactamente eso puede suceder y pasa en todas las implementaciones de threading que conozco. –

+0

@Steve: puede ser posible, pero es una mala idea. Hacer eso hace que la máquina abstracta * que * ha implementado viole el §1.9. Del mismo modo, puedo pasar un puntero a una variable no volátil al kernel y solicitarle E/S. Eso no hace que mi implementación en C++ no cumpla, es mi error. – Potatoswatter

0

Si su implementación C++ suministra la biblioteca de operaciones atómicas especificadas por n2145 o alguna variante de la misma, puede presumiblemente confiar en ella. De lo contrario, en general no puede confiar en "nada" sobre la atomicidad en el nivel del idioma, ya que la multitarea de cualquier tipo (y por lo tanto la atomicidad, que se ocupa de la multitarea) no está especificada por el estándar existente de C++.

0

Volatile en C++ no juega el mismo papel que en Java. Todos los casos son un comportamiento indefinido, como dice Steve. Algunos casos pueden ser correctos para un compilador, una arquitectura de procesador dada y un sistema de subprocesamiento múltiple, pero al cambiar los indicadores de optimización puede hacer que su programa se comporte de manera diferente, ya que los compiladores de C++ 03 no conocen los subprocesos.

C++ 0x define las reglas que evitan las condiciones de carrera y las operaciones que lo ayudan a dominarlo, pero puede que aún no haya un compilador que implemente toda la parte de la norma relacionada con este tema.

1

En general, es una muy, muy mala idea para depender de esto, ya que podría terminar con cosas malas que suceden y solo una algunas arquitecturas. La mejor solución sería usar una API atómica garantizada, por ejemplo, la API de Windows Interlocked.

6

La mayoría de las respuestas abordan correctamente los problemas de ordenamiento de memoria de la CPU que experimentará, pero ninguna ha explicado cómo el compilador puede frustrar sus intenciones al reordenar su código de forma que se rompen sus suposiciones.

Considere un ejemplo tomado de this post:

volatile int ready;  
int message[100];  

void foo(int i) 
{  
    message[i/10] = 42;  
    ready = 1;  
} 

En -O2 y por encima, las versiones recientes de GCC e Intel C/C++ (no sabe acerca de VC++) hará la tienda a ready en primer lugar, por lo que se pueden superponer con el cálculo de i/10 (volatile no te salva!):

leaq _message(%rip), %rax 
    movl $1, _ready(%rip)  ; <-- whoa Nelly! 
    movq %rsp, %rbp 
    sarl $2, %edx 
    subl %edi, %edx 
    movslq %edx,%rdx 
    movl $42, (%rax,%rdx,4) 

Esto no es un error, es el optimizador de la explotación de la canalización de la CPU. Si otro hilo está esperando en ready antes de acceder al contenido de message, entonces tiene una carrera desagradable y oscura.

Emplee las barreras del compilador para garantizar que se respete su intención. Un ejemplo que también explota la relativamente fuerte ordenamiento de x86 son la liberación/consumir envoltorios que se encuentran en un solo Productor cola de un solo Consumidor de Dmitriy Vyukov posted here:

// load with 'consume' (data-dependent) memory ordering 
// NOTE: x86 specific, other platforms may need additional memory barriers 
template<typename T> 
T load_consume(T const* addr) 
{ 
    T v = *const_cast<T const volatile*>(addr); 
    __asm__ __volatile__ ("" ::: "memory"); // compiler barrier 
    return v; 
} 

// store with 'release' memory ordering 
// NOTE: x86 specific, other platforms may need additional memory barriers 
template<typename T> 
void store_release(T* addr, T v) 
{ 
    __asm__ __volatile__ ("" ::: "memory"); // compiler barrier 
    *const_cast<T volatile*>(addr) = v; 
} 

Sugiero que si usted va a aventurarse en el ámbito de la Acceso a memoria concurrente, use una biblioteca que se encargará de estos detalles por usted. Mientras todos esperamos n2145 y std::atomic echa un vistazo a los bloques de construcción de subprocesos 'tbb::atomic o al próximo boost::atomic.

Además de la corrección, estas bibliotecas pueden simplificar el código y aclarar sus intenciones:

// thread 1 
std::atomic<int> foo; // or tbb::atomic, boost::atomic, etc 
foo.store(1, std::memory_order_release); 

// thread 2 
int tmp = foo.load(std::memory_order_acquire); 

Usando pedido memoria explícita, la relación entre hilos foo 's es clara.

2

Puede que este hilo sea antiguo, pero el estándar C++ 11 TIENE una biblioteca de hilos y también una amplia biblioteca atómica para operaciones atómicas. El propósito es específicamente para compatibilidad de concurrencia y evitar carreras de datos. La cabecera relevante es atómica

+0

Puede usar 'std :: atomic ' en lugar de 'int' para hacerlo atómico. Ver http://en.cppreference.com/w/cpp/atomic/atomic. – JojOatXGME

0

Mi respuesta va a ser frustrante: No, No, No, No y No.

1-4) Se permite que el compilador para hacer lo que le plazca con una variable a la que escribe Puede almacenar valores temporales en él, siempre que termine haciendo algo que haría lo mismo que ese hilo ejecutándose en el vacío. CUALQUIER COSA es válida

5) No, no hay garantía. Si una variable no es atómica, y usted escribe en un hilo, y la lee o escribe en otro, es un caso de carrera. La especificación declara que estos casos de raza son un comportamiento indefinido, y absolutamente todo vale. Dicho esto, será difícil encontrar un compilador que no le proporcione 7 u 8, pero ES legal que un compilador le proporcione otra cosa.

Siempre me refiero a esta explicación muy cómica de casos de carrera.

http://software.intel.com/en-us/blogs/2013/01/06/benign-data-races-what-could-possibly-go-wrong

Cuestiones relacionadas