C ++ 11 introdujo un modelo de memoria estandarizado. Qué significa eso? ¿Y cómo va a afectar la programación en C ++?

1894

C ++ 11 introdujo un modelo de memoria estandarizado, pero ¿qué significa eso exactamente? ¿Y cómo va a afectar la programación en C ++?

Este artículo (de Gavin Clarke, quien cita a Herb Sutter ) dice que,

El modelo de memoria significa que el código C ++ ahora tiene una biblioteca estandarizada para llamar, independientemente de quién hizo el compilador y en qué plataforma se está ejecutando. Hay una forma estándar de controlar cómo los diferentes hilos se comunican con la memoria del procesador.

"Cuando se habla de dividir [el código] en diferentes núcleos que están en el estándar, estamos hablando del modelo de memoria. Vamos a optimizarlo sin romper los siguientes supuestos que la gente hará en el código", dijo Sutter .

Bueno, puedo memorizar este y otros párrafos similares disponibles en línea (ya que tengo mi propio modelo de memoria desde mi nacimiento: P) e incluso puedo publicar como respuesta a las preguntas formuladas por otros, pero para ser sincero, no entiendo exactamente esta.

Los programadores de C ++ solían desarrollar aplicaciones de subprocesos múltiples incluso antes, entonces, ¿qué importancia tiene si son hilos POSIX, Windows o C ++ 11? ¿Cuales son los beneficios? Quiero entender los detalles de bajo nivel.

También tengo la sensación de que el modelo de memoria C ++ 11 está relacionado de alguna manera con el soporte de subprocesos múltiples de C ++ 11, ya que a menudo los veo juntos. Si es así, ¿cómo exactamente? ¿Por qué deberían estar relacionados?

Como no sé cómo funcionan las partes internas de subprocesos múltiples y qué significa el modelo de memoria en general, por favor, ayúdenme a comprender estos conceptos. :-)

Nawaz
fuente
3
@curiousguy: Elaborate ...
Nawaz
44
@curiousguy: Escribe un blog entonces ... y propone una solución también. No hay otra forma de hacer que su punto sea válido y racional.
Nawaz
2
Confundí ese sitio como un lugar para preguntar Q e intercambiar ideas. Culpa mía; Es un lugar de conformidad donde no puedes estar en desacuerdo con Herb Sutter, incluso cuando él se contradice flagrantemente sobre las especificaciones de lanzamiento.
curioso
55
@curiousguy: C ++ es lo que dice el estándar, no lo que dice un tipo aleatorio en Internet. Entonces sí, tiene que haber conformidad con el Estándar. C ++ NO es una filosofía abierta en la que se pueda hablar de cualquier cosa que no se ajuste al Estándar.
Nawaz
3
"Probé que ningún programa C ++ puede tener un comportamiento bien definido". . ¡Grandes reclamos, sin ninguna prueba!
Nawaz

Respuestas:

2205

Primero, debes aprender a pensar como un abogado de idiomas.

La especificación C ++ no hace referencia a ningún compilador, sistema operativo o CPU en particular. Hace referencia a una máquina abstracta que es una generalización de sistemas reales. En el mundo de Law Lawyer, el trabajo del programador es escribir código para la máquina abstracta; El trabajo del compilador es actualizar ese código en una máquina concreta. Al codificar rígidamente la especificación, puede estar seguro de que su código se compilará y ejecutará sin modificaciones en cualquier sistema con un compilador C ++ compatible, ya sea hoy o dentro de 50 años.

La máquina abstracta en la especificación C ++ 98 / C ++ 03 es fundamentalmente de un solo subproceso. Por lo tanto, no es posible escribir código C ++ multiproceso que sea "totalmente portátil" con respecto a la especificación. La especificación ni siquiera dice nada sobre la atomicidad de las cargas y las tiendas de memoria o el orden en que pueden ocurrir las cargas y las tiendas, no importa cosas como mutexes.

Por supuesto, puede escribir código multiproceso en la práctica para sistemas concretos concretos, como pthreads o Windows. Pero no hay una forma estándar de escribir código multiproceso para C ++ 98 / C ++ 03.

La máquina abstracta en C ++ 11 es multiproceso por diseño. También tiene un modelo de memoria bien definido ; es decir, dice lo que el compilador puede y no puede hacer cuando se trata de acceder a la memoria.

Considere el siguiente ejemplo, donde dos hilos acceden simultáneamente a un par de variables globales:

           Global
           int x, y;

Thread 1            Thread 2
x = 17;             cout << y << " ";
y = 37;             cout << x << endl;

¿Cuál podría ser la salida del hilo 2?

Bajo C ++ 98 / C ++ 03, esto ni siquiera es un comportamiento indefinido; la pregunta en sí misma no tiene sentido porque el estándar no contempla nada llamado "hilo".

Bajo C ++ 11, el resultado es Comportamiento indefinido, porque las cargas y las tiendas no necesitan ser atómicas en general. Lo que puede no parecer una gran mejora ... Y por sí solo, no lo es.

Pero con C ++ 11, puedes escribir esto:

           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;

Ahora las cosas se ponen mucho más interesantes. En primer lugar, el comportamiento aquí está definido . El subproceso 2 ahora podría imprimirse 0 0(si se ejecuta antes del subproceso 1), 37 17(si se ejecuta después del subproceso 1) o 0 17(si se ejecuta después del subproceso 1 se asigna a x pero antes de que se asigne a y).

Lo que no puede imprimir es 37 0porque el modo predeterminado para las cargas / almacenes atómicos en C ++ 11 es imponer una coherencia secuencial . Esto solo significa que todas las cargas y almacenes deben ser "como si" ocurrieran en el orden en que las escribió dentro de cada hilo, mientras que las operaciones entre hilos se pueden intercalar como quiera el sistema. Por lo tanto, el comportamiento predeterminado de los atómicos proporciona atomicidad y pedidos para cargas y tiendas.

Ahora, en una CPU moderna, garantizar la coherencia secuencial puede ser costoso. En particular, es probable que el compilador emita barreras de memoria entre todos los accesos aquí. Pero si su algoritmo puede tolerar cargas y tiendas fuera de servicio; es decir, si requiere atomicidad pero no ordenar; es decir, si puede tolerar 37 0como salida de este programa, entonces puede escribir esto:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_relaxed);   cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed);   cout << x.load(memory_order_relaxed) << endl;

Cuanto más moderna sea la CPU, más probable será que sea más rápida que en el ejemplo anterior.

Finalmente, si solo necesita mantener en orden determinadas cargas y tiendas, puede escribir:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_release);   cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release);   cout << x.load(memory_order_acquire) << endl;

Esto nos lleva de vuelta a las cargas y tiendas ordenadas, por 37 0lo que ya no es una salida posible, pero lo hace con una sobrecarga mínima. (En este ejemplo trivial, el resultado es el mismo que la consistencia secuencial completa; en un programa más amplio, no lo sería).

Por supuesto, si las únicas salidas que desea ver son 0 0o 37 17, puede envolver un mutex alrededor del código original. Pero si has leído hasta aquí, apuesto a que ya sabes cómo funciona, y esta respuesta ya es más larga de lo que pretendía :-).

Entonces, el resultado final. Los mutexes son geniales y C ++ 11 los estandariza. Pero a veces, por razones de rendimiento, desea primitivas de nivel inferior (p. Ej., El clásico patrón de bloqueo de doble verificación ). El nuevo estándar proporciona dispositivos de alto nivel como mutexes y variables de condición, y también proporciona dispositivos de bajo nivel como tipos atómicos y los diversos sabores de la barrera de la memoria. Entonces, ahora puede escribir rutinas concurrentes sofisticadas y de alto rendimiento completamente dentro del lenguaje especificado por el estándar, y puede estar seguro de que su código se compilará y se ejecutará sin cambios tanto en los sistemas actuales como en los de mañana.

Aunque, para ser sincero, a menos que sea un experto y trabaje en algún código serio de bajo nivel, probablemente deba atenerse a mutexes y variables de condición. Eso es lo que pretendo hacer.

Para más información sobre estas cosas, vea esta publicación de blog .

Nemo
fuente
37
Buena respuesta, pero esto realmente está pidiendo algunos ejemplos reales de las nuevas primitivas. Además, creo que el orden de la memoria sin primitivas es el mismo que antes de C ++ 0x: no hay garantías.
John Ripley
55
@ John: Lo sé, pero todavía estoy aprendiendo las primitivas :-). También creo que garantizan que los accesos de bytes son atómicos (aunque no ordenados), por eso elegí "char" para mi ejemplo ... Pero ni siquiera estoy 100% seguro de eso ... Si quieres sugerir algo bueno " tutorial "referencias Las agregaré a mi respuesta
Nemo
48
@Nawaz: ¡Sí! El compilador o la CPU pueden reordenar los accesos a la memoria. Piense en (por ejemplo) cachés y cargas especulativas. El orden en que se acierta la memoria del sistema no puede ser similar al que codificó. El compilador y la CPU asegurarán que tales reordenamientos no rompan el código de un solo subproceso . Para el código de subprocesos múltiples, el "modelo de memoria" caracteriza los posibles reordenamientos, y qué sucede si dos hilos leen / escriben en la misma ubicación al mismo tiempo, y cómo excede el control sobre ambos. Para el código de subproceso único, el modelo de memoria es irrelevante.
Nemo
26
@Nawaz, @Nemo: un detalle menor: el nuevo modelo de memoria es relevante en código de subproceso único en la medida en que especifica la indefinición de ciertas expresiones, como i = i++. El viejo concepto de puntos de secuencia ha sido descartado; El nuevo estándar especifica lo mismo usando una relación secuenciada antes , que es solo un caso especial del concepto más general entre hilos que sucede antes .
JohannesD
17
@ AJG85: La sección 3.6.2 del borrador de la especificación C ++ 0x dice: "Las variables con duración de almacenamiento estático (3.7.1) o duración de almacenamiento de subprocesos (3.7.2) se inicializarán en cero (8.5) antes de que se realice cualquier otra inicialización sitio." Dado que x, y son globales en este ejemplo, tienen una duración de almacenamiento estático y, por lo tanto, se inicializarán en cero, creo.
Nemo
345

Solo daré la analogía con la que entiendo los modelos de consistencia de memoria (o modelos de memoria, para abreviar). Está inspirado en el artículo seminal de Leslie Lamport "Time, Clocks, and the Ordering of Events in a Distributed System" . La analogía es adecuada y tiene un significado fundamental, pero puede ser exagerada para muchas personas. Sin embargo, espero que proporcione una imagen mental (una representación pictórica) que facilite el razonamiento sobre los modelos de consistencia de la memoria.

Veamos los historiales de todas las ubicaciones de memoria en un diagrama de espacio-tiempo en el que el eje horizontal representa el espacio de direcciones (es decir, cada ubicación de memoria está representada por un punto en ese eje) y el eje vertical representa el tiempo (veremos que, en general, no existe una noción universal del tiempo). El historial de valores que posee cada ubicación de memoria está, por lo tanto, representado por una columna vertical en esa dirección de memoria. Cada cambio de valor se debe a que uno de los hilos escribe un nuevo valor en esa ubicación. Por una imagen de memoria , nos referiremos al agregado / combinación de valores de todas las ubicaciones de memoria observables en un momento particular por un hilo particular .

Cita de "Una cartilla sobre consistencia de memoria y coherencia de caché"

El modelo de memoria intuitivo (y más restrictivo) es la consistencia secuencial (SC) en la que una ejecución multiproceso debería verse como un entrelazado de las ejecuciones secuenciales de cada subproceso constituyente, como si los subprocesos fueran multiplexados en el tiempo en un procesador de un solo núcleo.

Ese orden de memoria global puede variar de una ejecución del programa a otra y puede no conocerse de antemano. El rasgo característico de SC es el conjunto de cortes horizontales en el diagrama de dirección espacio-tiempo que representan planos de simultaneidad (es decir, imágenes de memoria). En un plano dado, todos sus eventos (o valores de memoria) son simultáneos. Existe una noción de tiempo absoluto , en la que todos los hilos acuerdan qué valores de memoria son simultáneos. En SC, en cada instante, solo hay una imagen de memoria compartida por todos los hilos. Es decir, en cada instante de tiempo, todos los procesadores acuerdan la imagen de la memoria (es decir, el contenido agregado de la memoria). Esto no solo implica que todos los subprocesos ven la misma secuencia de valores para todas las ubicaciones de memoria, sino que todos los procesadores observan lo mismocombinaciones de valores de todas las variables. Esto es lo mismo que decir que todas las operaciones de memoria (en todas las ubicaciones de memoria) se observan en el mismo orden total por todos los subprocesos.

En los modelos de memoria relajada, cada subproceso dividirá la dirección-espacio-tiempo a su manera, la única restricción es que los segmentos de cada subproceso no se cruzarán entre sí porque todos los subprocesos deben coincidir en el historial de cada ubicación de memoria individual (por supuesto , las rebanadas de diferentes hilos pueden cruzarse y se cruzarán entre sí). No hay una forma universal de dividirlo (no hay una foliación privilegiada de dirección-espacio-tiempo). Los cortes no tienen que ser planos (o lineales). Pueden ser curvos y esto es lo que puede hacer que un hilo lea los valores escritos por otro hilo fuera del orden en que fueron escritos. Los historiales de diferentes ubicaciones de memoria pueden deslizarse (o estirarse) arbitrariamente entre sí cuando son vistos por cualquier hilo particular.. Cada hilo tendrá un sentido diferente de qué eventos (o, equivalentemente, valores de memoria) son simultáneos. El conjunto de eventos (o valores de memoria) que son simultáneos a un hilo no son simultáneos a otro. Por lo tanto, en un modelo de memoria relajada, todos los hilos siguen observando el mismo historial (es decir, secuencia de valores) para cada ubicación de memoria. Pero pueden observar diferentes imágenes de memoria (es decir, combinaciones de valores de todas las ubicaciones de memoria). Incluso si dos ubicaciones de memoria diferentes están escritas por el mismo hilo en secuencia, los otros dos valores pueden observar los dos valores recién escritos en un orden diferente.

[Imagen de Wikipedia] Imagen de Wikipedia

Los lectores familiarizados con la Teoría especial de la relatividad de Einstein notarán a qué me estoy refiriendo. Traduciendo las palabras de Minkowski en el reino de los modelos de memoria: el espacio de direcciones y el tiempo son sombras del espacio de direcciones-tiempo. En este caso, cada observador (es decir, hilo) proyectará sombras de eventos (es decir, almacenes / cargas de memoria) en su propia línea mundial (es decir, su eje de tiempo) y su propio plano de simultaneidad (su eje de espacio de direcciones) . Los subprocesos en el modelo de memoria C ++ 11 corresponden a observadores que se mueven entre sí en una relatividad especial. La coherencia secuencial corresponde al espacio-tiempo galileano (es decir, todos los observadores están de acuerdo en un orden absoluto de eventos y un sentido global de simultaneidad).

La semejanza entre los modelos de memoria y la relatividad especial se deriva del hecho de que ambos definen un conjunto de eventos parcialmente ordenados, a menudo llamado un conjunto causal. Algunos eventos (es decir, almacenes de memoria) pueden afectar (pero no verse afectados por) otros eventos. Un hilo C ++ 11 (u observador en física) no es más que una cadena (es decir, un conjunto totalmente ordenado) de eventos (por ejemplo, la memoria se carga y almacena en direcciones posiblemente diferentes).

En la relatividad, se restablece cierto orden en la imagen aparentemente caótica de eventos parcialmente ordenados, ya que el único ordenamiento temporal en el que todos los observadores acuerdan es el ordenamiento entre eventos "temporales" (es decir, aquellos eventos que en principio son conectables por cualquier partícula que se vuelve más lenta) que la velocidad de la luz en el vacío). Solo los eventos relacionados con el tiempo se ordenan invariablemente. Tiempo en física, Craig Callender .

En el modelo de memoria C ++ 11, se utiliza un mecanismo similar (el modelo de coherencia de adquisición-liberación) para establecer estas relaciones de causalidad local .

Para proporcionar una definición de la coherencia de la memoria y una motivación para abandonar el SC, citaré "Una introducción a la coherencia de la memoria y la coherencia de la memoria caché"

Para una máquina de memoria compartida, el modelo de consistencia de memoria define el comportamiento arquitectónicamente visible de su sistema de memoria. El criterio de corrección para un comportamiento de particiones de núcleo de procesador único entre " un resultado correcto " y " muchas alternativas incorrectas ". Esto se debe a que la arquitectura del procesador exige que la ejecución de un subproceso transforme un estado de entrada dado en un solo estado de salida bien definido, incluso en un núcleo fuera de servicio. Sin embargo, los modelos de coherencia de memoria compartida se refieren a las cargas y almacenes de múltiples subprocesos y generalmente permiten muchas ejecuciones correctas.mientras que rechaza muchos (más) incorrectos. La posibilidad de múltiples ejecuciones correctas se debe a que la ISA permite que múltiples hilos se ejecuten simultáneamente, a menudo con muchas posibles entrelazamientos legales de instrucciones de diferentes hilos.

Los modelos de consistencia de memoria relajada o débil están motivados por el hecho de que la mayoría de los ordenamientos de memoria en modelos fuertes son innecesarios. Si un subproceso actualiza diez elementos de datos y luego un indicador de sincronización, a los programadores generalmente no les importa si los elementos de datos se actualizan uno con respecto al otro, sino solo que todos los elementos de datos se actualizan antes de que se actualice el indicador (generalmente implementado usando las instrucciones de FENCE ) Los modelos relajados buscan capturar esta mayor flexibilidad de pedidos y preservar solo los pedidos que los programadores " requieren"Para obtener un mayor rendimiento y corrección de SC. Por ejemplo, en ciertas arquitecturas, cada núcleo utiliza las memorias intermedias de escritura FIFO para guardar los resultados de las tiendas comprometidas (retiradas) antes de escribir los resultados en los cachés. Esta optimización mejora el rendimiento pero viola SC. El búfer de escritura oculta la latencia del servicio de una tienda perdida. Debido a que las tiendas son comunes, poder evitar el estancamiento en la mayoría de ellas es un beneficio importante. Para un procesador de un solo núcleo, un búfer de escritura se puede hacer arquitectónicamente invisible al garantizar que una carga en la dirección A devuelva el valor del almacén más reciente a A incluso si uno o más almacenes en A están en el búfer de escritura. Esto normalmente se hace omitiendo el valor de la tienda más reciente a A a la carga de A, donde "más reciente" se determina por orden de programa, o deteniendo una carga de A si hay un almacén en A en el búfer de escritura. Cuando se utilizan múltiples núcleos, cada uno tendrá su propio búfer de escritura de derivación. Sin memorias intermedias de escritura, el hardware es SC, pero con las memorias intermedias de escritura, no lo es, lo que hace que las memorias intermedias de escritura sean arquitectónicamente visibles en un procesador multinúcleo.

El reordenamiento tienda-tienda puede ocurrir si un núcleo tiene un búfer de escritura que no es FIFO que permite que las tiendas salgan en un orden diferente al orden en que ingresaron. Esto puede ocurrir si la primera tienda se pierde en la memoria caché mientras la segunda visita o si la segunda tienda puede fusionarse con una tienda anterior (es decir, antes de la primera tienda). El reordenamiento de carga-carga también puede ocurrir en núcleos programados dinámicamente que ejecutan instrucciones fuera del orden del programa. Eso puede comportarse igual que reordenar almacenes en otro núcleo (¿Puedes encontrar un ejemplo entrelazado entre dos subprocesos?). Reordenar una carga anterior con un almacén posterior (un reordenamiento de almacén de carga) puede causar muchos comportamientos incorrectos, como cargar un valor después de liberar el bloqueo que lo protege (si el almacén es la operación de desbloqueo).

Debido a que la coherencia de la memoria caché y la coherencia de la memoria a veces se confunden, es instructivo tener también esta cita:

A diferencia de la coherencia, la coherencia de la memoria caché no es visible para el software ni es necesaria. Coherence busca hacer que las memorias caché de un sistema de memoria compartida sean funcionalmente invisibles como las memorias caché en un sistema de un solo núcleo. La coherencia correcta asegura que un programador no pueda determinar si un sistema tiene cachés y dónde lo hace analizando los resultados de cargas y almacenes. Esto se debe a que la coherencia correcta garantiza que los cachés nunca permitan un comportamiento funcional nuevo o diferente (los programadores aún pueden inferir la estructura probable del caché usando el tiempoinformación). El objetivo principal de los protocolos de coherencia de caché es mantener invariante el escritor único-múltiples lectores (SWMR) para cada ubicación de memoria. Una distinción importante entre coherencia y consistencia es que la coherencia se especifica en una ubicación por memoria , mientras que la consistencia se especifica con respecto a todas las ubicaciones de memoria.

Continuando con nuestra imagen mental, el SWMR invariante corresponde al requisito físico de que haya como máximo una partícula ubicada en cualquier ubicación, pero puede haber un número ilimitado de observadores de cualquier ubicación.

Ahmed Nassar
fuente
52
+1 para la analogía con relatividad especial, he estado tratando de hacer la misma analogía. Demasiado a menudo veo programadores que investigan código enhebrado tratando de interpretar el comportamiento como operaciones en diferentes subprocesos que ocurren entrelazadas entre sí en un orden específico, y tengo que decirles, no, con sistemas multiprocesador la noción de simultaneidad entre diferentes <s > marcos de referencia </s> hilos ahora no tiene sentido. La comparación con la relatividad especial es una buena manera de hacer que respeten la complejidad del problema.
Pierre Lebeaupin
71
Entonces, ¿debes concluir que el Universo es multinúcleo?
Peter K
66
@PeterK: Exactamente :) Y aquí hay una muy buena visualización de esta imagen del tiempo por el físico Brian Greene: youtube.com/watch?v=4BjGWLJNPcA&t=22m12s Esta es "La ilusión del tiempo [Documental completo]" en el minuto 22 y 12 segundos
Ahmed Nassar
2
¿Soy solo yo o está cambiando de un modelo de memoria 1D (eje horizontal) a un modelo de memoria 2D (planos de simultaneidad). Esto me parece un poco confuso, pero tal vez sea porque no soy un hablante nativo ... Sigue siendo una lectura muy interesante.
Adiós SE
Olvidó una parte esencial: " al analizar los resultados de cargas y almacenes " ... sin utilizar información precisa de sincronización.
curioso
115

Esta es ahora una pregunta de varios años, pero siendo muy popular, vale la pena mencionar un recurso fantástico para aprender sobre el modelo de memoria C ++ 11. No veo ningún punto en resumir su charla para hacer esta otra respuesta completa, pero dado que este es el tipo que realmente escribió el estándar, creo que vale la pena ver la charla.

Herb Sutter tiene una charla de tres horas sobre el modelo de memoria C ++ 11 titulado "Armas atómicas <>", disponible en el sitio Channel9 - parte 1 y parte 2 . La charla es bastante técnica y cubre los siguientes temas:

  1. Optimizaciones, carreras y el modelo de memoria
  2. Pedidos - Qué: Adquirir y liberar
  3. Pedidos: cómo: mutexes, atómicos y / o cercas
  4. Otras restricciones en compiladores y hardware
  5. Código Gen & Performance: x86 / x64, IA64, POWER, ARM
  6. Atómica Relajada

La charla no profundiza en la API, sino más bien en el razonamiento, los antecedentes, bajo el capó y detrás de escena (¿Sabía que se agregó una semántica relajada al estándar solo porque POWER y ARM no admiten la carga sincronizada de manera eficiente?).

eran
fuente
10
Esa charla es realmente fantástica, vale la pena las 3 horas que pasará mirándola.
ZunTzu
55
@ZunTzu: en la mayoría de los reproductores de video puede configurar la velocidad a 1.25, 1.5 o incluso 2 veces la original.
Christian Severin
44
@eran ¿Ustedes tienen las diapositivas? los enlaces en las páginas de discusión del canal 9 no funcionan.
athos
2
@athos No los tengo, lo siento. Intente ponerse en contacto con el canal 9, no creo que la eliminación haya sido intencional (supongo que obtuvieron el enlace de Herb Sutter, publicado como está, y luego eliminó los archivos; pero eso es solo una especulación ...).
eran
75

Significa que el estándar ahora define subprocesos múltiples y define lo que sucede en el contexto de subprocesos múltiples. Por supuesto, las personas usaban diferentes implementaciones, pero eso es como preguntar por qué deberíamos tener un std::stringmomento en el que todos podríamos estar usando una stringclase casera .

Cuando habla de hilos POSIX o hilos de Windows, esto es una ilusión, ya que en realidad habla de hilos x86, ya que es una función de hardware que se ejecuta simultáneamente. El modelo de memoria C ++ 0x ofrece garantías, ya sea que esté en x86, o ARM, o MIPS , o cualquier otra cosa que se le ocurra.

Perrito
fuente
28
Los hilos de Posix no están restringidos a x86. De hecho, los primeros sistemas en los que se implementaron probablemente no eran sistemas x86. Los hilos de Posix son independientes del sistema y son válidos en todas las plataformas de Posix. Tampoco es realmente cierto que sea una propiedad de hardware porque los hilos Posix también se pueden implementar a través de la multitarea cooperativa. Pero, por supuesto, la mayoría de los problemas de subprocesos solo surgen en implementaciones de subprocesos de hardware (y algunos incluso solo en sistemas multiprocesador / multinúcleo).
celtschk
57

Para idiomas que no especifican un modelo de memoria, está escribiendo código para el idioma y el modelo de memoria especificado por la arquitectura del procesador. El procesador puede optar por reordenar los accesos de memoria para el rendimiento. Entonces, si su programa tiene carreras de datos (una carrera de datos es cuando es posible que múltiples núcleos / hiperprocesos accedan simultáneamente a la misma memoria), entonces su programa no es multiplataforma debido a su dependencia del modelo de memoria del procesador. Puede consultar los manuales del software Intel o AMD para averiguar cómo los procesadores pueden reordenar los accesos a la memoria.

Muy importante, los bloqueos (y la semántica de concurrencia con bloqueo) generalmente se implementan de forma multiplataforma ... Entonces, si está utilizando bloqueos estándar en un programa multiproceso sin carreras de datos, entonces no tiene que preocuparse por los modelos de memoria multiplataforma .

Curiosamente, los compiladores de Microsoft para C ++ tienen una semántica de adquisición / lanzamiento de volátil, que es una extensión de C ++ para tratar la falta de un modelo de memoria en C ++ http://msdn.microsoft.com/en-us/library/12a04hfd(v=vs .80) .aspx . Sin embargo, dado que Windows solo se ejecuta en x86 / x64, eso no dice mucho (los modelos de memoria Intel y AMD hacen que sea fácil y eficiente implementar la semántica de adquisición / lanzamiento en un idioma).

ritesh
fuente
2
Es cierto que, cuando se escribió la respuesta, Windows se ejecuta solo en x86 / x64, pero Windows se ejecuta, en algún momento, en IA64, MIPS, Alpha AXP64, PowerPC y ARM. Hoy se ejecuta en varias versiones de ARM, que es bastante diferente en cuanto a memoria de x86, y no es tan indulgente.
Lorenzo Dematté
Ese enlace está algo roto (dice "Documentación retirada de Visual Studio 2005" ). ¿Quieres actualizarlo?
Peter Mortensen
3
No era cierto incluso cuando se escribió la respuesta.
Ben
" para acceder a la misma memoria al mismo tiempo " para acceder de una manera conflictiva
curioso el
27

Si usa mutexes para proteger todos sus datos, realmente no debería preocuparse. Mutexes siempre ha proporcionado suficientes garantías de orden y visibilidad.

Ahora, si utilizó algoritmos atómicos o sin bloqueo, debe pensar en el modelo de memoria. El modelo de memoria describe con precisión cuándo los atómicos brindan garantías de orden y visibilidad, y proporciona cercas portátiles para garantías codificadas a mano.

Anteriormente, los atómicos se harían usando intrínsecos del compilador, o alguna biblioteca de nivel superior. Las cercas se habrían hecho usando instrucciones específicas de la CPU (barreras de memoria).

ninjalj
fuente
19
El problema anterior era que no existía un mutex (en términos del estándar C ++). Por lo tanto, las únicas garantías que le proporcionó fueron del fabricante de mutex, lo cual estuvo bien siempre que no haya transferido el código (ya que los cambios menores en las garantías son difíciles de detectar). Ahora tenemos garantías proporcionadas por el estándar que debería ser portátil entre plataformas.
Martin York
44
@ Martin: en cualquier caso, una cosa es el modelo de memoria y otra son las primitivas atómicas y de subprocesos que se ejecutan sobre ese modelo de memoria.
ninjalj
44
Además, mi punto era principalmente que anteriormente no había un modelo de memoria en el nivel del lenguaje, sino que era el modelo de memoria de la CPU subyacente. Ahora hay un modelo de memoria que es parte del lenguaje central; OTOH, mutexes y similares siempre se pueden hacer como una biblioteca.
ninjalj
3
También podría ser un problema real para las personas que intentan escribir la biblioteca mutex. Cuando la CPU, el controlador de memoria, el kernel, el compilador y la "biblioteca C" son implementados por diferentes equipos, y algunos de ellos están en desacuerdo violento en cuanto a cómo se supone que funcionan estas cosas, bueno, a veces las cosas nosotros, los programadores de sistemas, tenemos que hacer para presentar una bonita fachada a nivel de aplicaciones que no es nada agradable.
zwol
11
Desafortunadamente, no es suficiente proteger sus estructuras de datos con mutexes simples si no hay un modelo de memoria consistente en su idioma. Hay varias optimizaciones del compilador que tienen sentido en un contexto de subproceso único, pero cuando entran en juego varios subprocesos y núcleos de CPU, la reordenación de los accesos a la memoria y otras optimizaciones pueden generar un comportamiento indefinido. Para obtener más información, consulte "Los subprocesos no pueden implementarse como una biblioteca" por Hans Boehm: citeseer.ist.psu.edu/viewdoc/…
exDM69
0

Las respuestas anteriores llegan a los aspectos más fundamentales del modelo de memoria C ++. En la práctica, la mayoría de los usos de std::atomic<>"solo trabajo", al menos hasta que el programador optimiza en exceso (por ejemplo, tratando de relajar demasiadas cosas).

Hay un lugar donde los errores siguen siendo comunes: bloqueos de secuencia . Hay una discusión excelente y fácil de leer sobre los desafíos en https://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf . Los bloqueos de secuencia son atractivos porque el lector evita escribir en la palabra de bloqueo. El siguiente código se basa en la Figura 1 del informe técnico anterior y destaca los desafíos al implementar bloqueos de secuencia en C ++:

atomic<uint64_t> seq; // seqlock representation
int data1, data2;     // this data will be protected by seq

T reader() {
    int r1, r2;
    unsigned seq0, seq1;
    while (true) {
        seq0 = seq;
        r1 = data1; // INCORRECT! Data Race!
        r2 = data2; // INCORRECT!
        seq1 = seq;

        // if the lock didn't change while I was reading, and
        // the lock wasn't held while I was reading, then my
        // reads should be valid
        if (seq0 == seq1 && !(seq0 & 1))
            break;
    }
    use(r1, r2);
}

void writer(int new_data1, int new_data2) {
    unsigned seq0 = seq;
    while (true) {
        if ((!(seq0 & 1)) && seq.compare_exchange_weak(seq0, seq0 + 1))
            break; // atomically moving the lock from even to odd is an acquire
    }
    data1 = new_data1;
    data2 = new_data2;
    seq = seq0 + 2; // release the lock by increasing its value to even
}

Tan poco intuitivo como parece al principio, data1y data2debe serlo atomic<>. Si no son atómicos, entonces podrían leerse (in reader()) al mismo tiempo que se escriben (in writer()). Según el modelo de memoria de C ++, esta es una carrera, incluso si reader()nunca utiliza los datos . Además, si no son atómicos, el compilador puede almacenar en caché la primera lectura de cada valor en un registro. Obviamente no querrás eso ... quieres volver a leer en cada iteración del whilebucle reader().

Tampoco es suficiente hacerlos atomic<>y acceder a ellos con memory_order_relaxed. La razón de esto es que las lecturas de seq (in reader()) solo tienen semántica adquirida . En términos simples, si X e Y son accesos de memoria, X precede a Y, X no es una adquisición o liberación, e Y es una adquisición, entonces el compilador puede reordenar Y antes de X. Si Y fue la segunda lectura de seq, y X era una lectura de datos, tal reordenamiento rompería la implementación del bloqueo.

El artículo da algunas soluciones. El que tiene el mejor rendimiento hoy es probablemente el que usa un atomic_thread_fencecon memory_order_relaxed antes de la segunda lectura del seqlock. En el documento, es la Figura 6. No estoy reproduciendo el código aquí, porque cualquiera que haya leído hasta aquí realmente debería leer el documento. Es más preciso y completo que este post.

El último problema es que podría no ser natural hacer que las datavariables sean atómicas. Si no puede en su código, entonces debe tener mucho cuidado, porque la conversión de no atómico a atómico solo es legal para los tipos primitivos. Se supone que C ++ 20 agrega atomic_ref<>, lo que hará que este problema sea más fácil de resolver.

Para resumir: incluso si cree que comprende el modelo de memoria C ++, debe tener mucho cuidado antes de rodar sus propios bloqueos de secuencia.

Mike Spear
fuente
-2

C y C ++ solían definirse mediante un seguimiento de ejecución de un programa bien formado.

Ahora están medio definidos por un seguimiento de ejecución de un programa, y ​​medio a posteriori por muchos ordenamientos en objetos de sincronización.

Lo que significa que estas definiciones de lenguaje no tienen ningún sentido ya que no hay un método lógico para mezclar estos dos enfoques. En particular, la destrucción de un mutex o una variable atómica no está bien definida.

curioso
fuente
Comparto su feroz deseo de mejorar el diseño del lenguaje, pero creo que su respuesta sería más valiosa si se centrara en un caso simple, para el cual mostró clara y explícitamente cómo ese comportamiento viola los principios específicos del diseño del lenguaje. Después de eso, le recomendaría encarecidamente, si me lo permite, dar una respuesta muy buena sobre la relevancia de cada uno de esos puntos, porque se contrastará con la relevancia de los inmensos beneficios de productividad percibidos por el diseño de C ++
Matias Haeussler
1
@MatiasHaeussler Creo que leíste mal mi respuesta; No me opongo a la definición de una característica particular de C ++ aquí (también tengo muchas críticas puntiagudas pero no aquí). Estoy argumentando aquí que no hay una construcción bien definida en C ++ (ni C). Toda la semántica MT es un completo desastre, ya que ya no tienes semántica secuencial. (Creo que Java MT está roto pero menos). El "ejemplo simple" sería casi cualquier programa de MT. Si no está de acuerdo, puede responder a mi pregunta sobre cómo probar la corrección de los programas MT C ++ .
curioso
Interesante, creo que entiendo más a qué te refieres después de leer tu pregunta. Si tengo razón, se está refiriendo a la imposibilidad de desarrollar pruebas para la corrección de los programas C ++ MT . En tal caso, diría que para mí es algo de gran importancia para el futuro de la programación de computadoras, en particular para la llegada de la inteligencia artificial. Pero también señalaría que para la gran mayoría de las personas que hacen preguntas en el desbordamiento de la pila, eso no es algo de lo que ni siquiera se
den
1
"¿Deberían publicarse preguntas sobre la demostrabilidad de los programas de computadora en stackoverflow o en stackexchange (si en ninguno, dónde)?" Este parece ser uno para meta stackoverflow, ¿no es así?
Matias Haeussler
1
@MatiasHaeussler 1) C y C ++ comparten esencialmente el "modelo de memoria" de variables atómicas, mutexes y subprocesos múltiples. 2) La relevancia de esto es sobre los beneficios de tener el "modelo de memoria". Creo que el beneficio es cero ya que el modelo no es sólido.
curioso