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

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

Este artículo (Por Gavin Clarke quien cita Sutter de hierbas) 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. Existe una forma estándar de controlar cómo los diferentes subprocesos se comunican con la memoria del procesador.

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

Bueno, yo puedo memorizar este y otros párrafos similares están disponibles en línea (ya que tengo mi propio modelo de memoria desde que nací: P) e incluso puedo publicarlo como respuesta a preguntas de otros, pero para ser honesto, no entiendo exactamente esto.

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

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

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

preguntado el 11 de junio de 11 a las 23:06

@curiousguy: Escribe un blog entonces ... y propone una solución también. No hay otra forma de hacer que su punto de vista sea válido y racional. -

Confundí ese sitio con un lugar para preguntar Q e intercambiar ideas. Mi error; es un lugar para la conformidad en el que no se puede estar en desacuerdo con Herb Sutter incluso cuando se contradice flagrantemente a sí mismo sobre la especificación del lanzamiento. -

@curiousguy: C ++ es lo que dice el Estándar, no lo que dice un tipo cualquiera en Internet. Así que sí, tiene que haber conformidad con el Estándar. C ++ NO es una filosofía abierta en la que se puede hablar de cualquier cosa que no ajustarse al Estándar. -

"Demostré que ningún programa en C ++ puede tener un comportamiento bien definido".. ¡Reclamaciones altas, sin ninguna prueba! -

No. No he eliminado ninguna pregunta ni respuesta. De todos modos, las primitivas tienen ciertas garantías, ¿no? Si es así, entonces componga garantías más grandes basadas en esas garantías primitivas. De todos modos, ¿crees que es un problema en C ++ (y probablemente también en C) solo, o es un problema en TODOS los lenguajes? -

8 Respuestas

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 un máquina abstracta esa es una generalización de los sistemas reales. En el mundo de Language 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 según las especificaciones, puede estar seguro de que su código se compilará y se ejecutará sin modificaciones en cualquier sistema con un compilador compatible con C ++, 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 "completamente portátil" con respecto a la especificación. La especificación ni siquiera dice nada sobre el atomicidad de cargas y almacenes de memoria o solicite en el que pueden ocurrir cargas y tiendas, sin importar cosas como mutex.

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

La máquina abstracta en C ++ 11 tiene varios subprocesos por diseño. También tiene un bien definido modelo de memoria; 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 subprocesos 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;

¿Qué podría generar Thread 2?

En C ++ 98 / C ++ 03, esto ni siquiera es un comportamiento indefinido; la pregunta en sí es sin sentido porque la norma no contempla nada llamado "hilo".

En C ++ 11, el resultado es un comportamiento indefinido, porque las cargas y los almacenes no necesitan ser atómicos 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í es se define. El hilo 2 ahora podría imprimir 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 de que el subproceso 1 se asigne a x pero antes de que se asigne a y).

Lo que no puede imprimir es 37 0, porque el modo predeterminado para cargas atómicas / almacenes en C ++ 11 es hacer cumplir consistencia 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. Entonces, el comportamiento predeterminado de Atomics proporciona tanto atomicidad y pedido para cargas y almacenes.

Ahora, en una CPU moderna, garantizar la coherencia secuencial puede resultar caro. En particular, es probable que el compilador emita barreras de memoria en toda regla entre cada acceso aquí. Pero si su algoritmo puede tolerar cargas y almacenes fuera de orden; es decir, si requiere atomicidad pero no ordenación; es decir, si puede tolerar 37 0 como salida de este programa, 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, es más probable que sea más rápido que en el ejemplo anterior.

Finalmente, si solo necesita mantener determinadas cargas y tiendas en orden, 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 almacenes ordenados, por lo que 37 0 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 coherencia secuencial en toda regla; en un programa más grande, no lo sería).

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

Entonces, la línea de fondo. Los mutex son geniales y C ++ 11 los estandariza. Pero a veces, por razones de rendimiento, desea primitivas de nivel inferior (por ejemplo, el clásico patrón de bloqueo revisado minuciosamente). El nuevo estándar proporciona dispositivos de alto nivel como mutex y variables de condición, y también proporciona dispositivos de bajo nivel como tipos atómicos y los distintos tipos de barrera de memoria. Así que 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 del mañana.

Aunque para ser franco, a menos que sea un experto y esté trabajando en un código serio de bajo nivel, probablemente debería ceñirse a las mutex y las variables de condición. Eso es lo que pretendo hacer.

Para obtener más información sobre este tema, consulte esta entrada del blog.

Respondido el 17 de junio de 21 a las 02:06

Buena respuesta, pero esto realmente pide 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

@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 el que se alcanza la memoria del sistema no puede ser nada parecido al que codificó. El compilador y la CPU se asegurarán de que dichos reordenamientos no se rompan de un solo hilo código. Para el código de múltiples subprocesos, el "modelo de memoria" caracteriza los posibles reordenamientos y lo que sucede si dos subprocesos leen / escriben la misma ubicación al mismo tiempo, y cómo se ejerce el control sobre ambos. Para el código de un solo subproceso, el modelo de memoria es irrelevante. - Nemo

@Nawaz, @Nemo - Un detalle menor: el nuevo modelo de memoria es relevante en el código de un solo subproceso 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 un secuenciado antes relación que es solo un caso especial del inter-hilo más general sucede antes concepto. - JohannesD

@ 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) deben inicializarse a cero (8.5) antes de que se lleve a cabo cualquier otra inicialización. sitio." Dado que x, y son globales en este ejemplo, tienen una duración de almacenamiento estática y, por lo tanto, creo que se inicializarán en cero. - Nemo

@Bemipefe: No, el compilador no está obligado a traducir su código en el mismo orden en que lo escribió; puede reordenar las operaciones, siempre que el efecto general sea el mismo. Podría hacer esto, por ejemplo, porque el reordenamiento le permite producir código más rápido (o más pequeño). - psmears

Solo daré la analogía con la que entiendo los modelos de consistencia de la memoria (o modelos de memoria, para abreviar). Está inspirado en el artículo fundamental de Leslie Lamport. "Hora, relojes y ordenación de eventos en un sistema distribuido". La analogía es adecuada y tiene un significado fundamental, pero puede resultar excesiva 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 las historias de todas las ubicaciones de la memoria en un diagrama de espacio-tiempo en el que el eje horizontal representa el espacio de direcciones (es decir, cada ubicación de la 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 de tiempo). Por lo tanto, el historial de valores de cada ubicación de memoria está 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 un imagen de memoria, nos referiremos al agregado / combinación de valores de todas las ubicaciones de memoria observables en un momento particular by un hilo en particular.

Citando de "Introducción a la coherencia de la memoria y la coherencia de la caché"

El modelo de memoria intuitivo (y más restrictivo) es la consistencia secuencial (SC) en el que una ejecución multiproceso debería verse como un entrelazado de las ejecuciones secuenciales de cada subproceso constituyente, como si los subprocesos estuvieran 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 es posible que no se conozca de antemano. El rasgo característico de SC es el conjunto de cortes horizontales en el diagrama de dirección-espacio-tiempo que representa planos de simultaneidad (es decir, imágenes de memoria). En un plano dado, todos sus eventos (o valores de memoria) son simultáneos. Hay una noción de Tiempo absoluto, en el que todos los subprocesos coinciden en qué valores de memoria son simultáneos. En SC, en cada instante de tiempo, solo hay una imagen de memoria compartida por todos los subprocesos. Es decir, en cada instante de tiempo, todos los procesadores están de acuerdo con 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 también que todos los procesadores observan la misma combinaciones 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 hilo dividirá el espacio-tiempo de direcciones a su manera, la única restricción es que los segmentos de cada hilo no se cruzarán entre sí porque todos los hilos deben coincidir en el historial de cada ubicación de memoria individual (por supuesto , trozos de diferentes hilos pueden cruzarse, y se cruzarán). No hay una forma universal de dividirlo (no hay foliación privilegiada de dirección-espacio-tiempo). Las rebanadas no tienen que ser planas (o lineales). Se pueden curvar y esto es lo que puede hacer que un hilo lea valores escritos por otro hilo fuera del orden en el que fueron escritos. Los historiales de diferentes ubicaciones de memoria pueden deslizarse (o estirarse) arbitrariamente entre sí. cuando lo ve un hilo en particular. Cada hilo tendrá un sentido diferente de qué eventos (o, de manera equivalente, 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 relajado, todos los subprocesos siguen observando el mismo historial (es decir, secuencia de valores) para cada ubicación de memoria. Pero pueden observar diferentes imágenes de la memoria (es decir, combinaciones de valores de todas las ubicaciones de la memoria). Incluso si el mismo subproceso escribe dos ubicaciones de memoria diferentes en secuencia, los dos valores recién escritos pueden ser observados en un orden diferente por otros subprocesos.

[Imagen de Wikipedia] Imagen de Wikipedia

Lectores familiarizados con Einstein Teoría especial de la relatividad se dará cuenta de a qué me refiero. Traducir las palabras de Minkowski al 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, almacenamiento / carga de memoria) en su propia línea de mundo (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 relatividad especial. La consistencia secuencial corresponde a la 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 conjunto causal. Algunos eventos (es decir, los almacenes de memoria) pueden afectar (pero no verse afectados por) otros eventos. Un hilo de C ++ 11 (u observador en física) no es más que una cadena (es decir, un conjunto totalmente ordenado) de eventos (p. Ej., Cargas de memoria y almacenes en direcciones posiblemente diferentes).

En relatividad, se restaura algo de orden en la imagen aparentemente caótica de eventos parcialmente ordenados, ya que el único ordenamiento temporal en el que todos los observadores están de acuerdo es el ordenamiento entre eventos "temporales" (es decir, aquellos eventos que son en principio conectables por cualquier partícula que vaya más lentamente). 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 de C ++ 11, se utiliza un mecanismo similar (el modelo de coherencia de adquisición-liberación) para establecer estos relaciones de causalidad local.

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

Para una máquina de memoria compartida, el modelo de consistencia de memoria define el comportamiento visible desde el punto de vista arquitectónico de su sistema de memoria. El criterio de corrección para el comportamiento de particiones de un solo núcleo de procesador 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 estado de salida único bien definido, incluso en un núcleo desordenado. Sin embargo, los modelos de coherencia de memoria compartida se refieren a las cargas y almacenes de varios subprocesos y, por lo general, permiten muchas ejecuciones correctas mientras rechaza muchas (más) incorrectas. La posibilidad de múltiples ejecuciones correctas se debe a que ISA permite que varios subprocesos se ejecuten simultáneamente, a menudo con muchas posibles intercalaciones legales de instrucciones de diferentes subprocesos.

Relajado or débil Los modelos de consistencia de memoria están motivados por el hecho de que la mayoría de los ordenamientos de memoria en modelos fuertes son innecesarios. Si un hilo 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 en orden entre sí, sino solo que todos los elementos de datos se actualicen antes de que se actualice el indicador (generalmente implementado usando instrucciones FENCE ). Los modelos relajados buscan capturar esta mayor flexibilidad de pedidos y preservar solo los pedidos que los programadores "exigir”Para obtener un mayor rendimiento y precisión de SC. Por ejemplo, en ciertas arquitecturas, cada núcleo usa búferes de escritura FIFO para almacenar los resultados de las tiendas comprometidas (retiradas) antes de escribir los resultados en las cachés. Esta optimización mejora el rendimiento pero viola SC. El búfer de escritura oculta la latencia de dar servicio a un error de tienda. 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 asegurándose de 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 de A están en el búfer de escritura. Por lo general, esto 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 el orden del programa, o deteniendo una carga de A si una tienda de A está en el búfer de escritura. . Cuando se utilizan varios núcleos, cada uno tendrá su propio búfer de escritura de omisión. Sin búferes de escritura, el hardware es SC, pero con búferes de escritura no lo es, lo que hace que los búferes de escritura sean arquitectónicamente visibles en un procesador multinúcleo.

El reordenamiento de tienda-tienda puede ocurrir si un núcleo tiene un búfer de escritura no FIFO que permite que las tiendas salgan en un orden diferente al orden en el que entraron. Esto puede ocurrir si la primera tienda falla en la caché mientras que la segunda llega 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 de la misma manera que reordenar las tiendas en otro núcleo (¿se le ocurre un ejemplo de entrelazado entre dos subprocesos?). Reordenar una carga anterior con una tienda posterior (reordenar una tienda de carga) puede provocar muchos comportamientos incorrectos, como cargar un valor después de liberar el candado que lo protege (si la tienda es la operación de desbloqueo). Tenga en cuenta que los reordenamientos de la carga de la tienda también pueden surgir debido a la omisión local en el búfer de escritura FIFO comúnmente implementado, incluso con un núcleo que ejecuta todas las instrucciones en el orden del programa.

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 consistencia, coherencia de caché no es visible para el software ni es obligatorio. Coherence busca hacer que los cachés de un sistema de memoria compartida sean tan funcionalmente invisibles como los cachés de un sistema de un solo núcleo. La coherencia correcta garantiza que un programador no pueda determinar si un sistema tiene cachés y dónde se encuentra mediante el análisis de los resultados de las cargas y los almacenes. Esto se debe a que la coherencia correcta asegura que los cachés nunca habiliten nuevos o diferentes funcional comportamiento (los programadores aún pueden inferir una estructura de caché probable usando sincronización información). El propósito principal de los protocolos de coherencia de caché es mantener invariante un único escritor y varios lectores (SWMR) para cada ubicación de memoria. Una distinción importante entre coherencia y consistencia es que la coherencia se especifica en un base por ubicación de memoria, mientras que la consistencia se especifica con respecto a todos ubicaciones de memoria.

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

Respondido el 20 de junio de 20 a las 12:06

+1 para la analogía con la relatividad especial, he estado tratando de hacer la misma analogía yo mismo. Con demasiada frecuencia veo a programadores que investigan código enhebrado tratando de interpretar el comportamiento como operaciones en diferentes subprocesos que ocurren intercalados entre sí en un orden específico, y tengo que decirles, no, con sistemas multiprocesador la noción de simultaneidad entre diferentes marcos de Los hilos de referencia ahora no tienen sentido. Comparar con la relatividad especial es una buena forma de hacer que respeten la complejidad del problema. - Pierre Lebeaupin

Entonces, ¿debería concluir que el Universo es multinúcleo? - Peter K

@PeterK: Exactamente :) Y aquí hay una visualización muy bonita de esta imagen del tiempo del 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

Soy solo yo o está cambiando de un modelo de memoria 1D (eje horizontal) a un modelo de memoria 2D (planos de simultaneidad). Encuentro esto un poco confuso, pero tal vez se deba a que no soy un hablante nativo ... Sigue siendo una lectura muy interesante. - Kami Kaze

@Yibo Claro. Sentirse libre. - Ahmed Nassar

Esta es ahora una pregunta de varios años, pero al ser muy popular, vale la pena mencionar un recurso fantástico para aprender sobre el modelo de memoria C ++ 11. No veo ningún sentido en resumir su charla para dar 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 de Channel9: en la parte 1 y en la parte 2. La charla es bastante técnica y cubre los siguientes temas:

  1. Optimizaciones, carreras y modelo de memoria
  2. Pedidos - Qué: Adquirir y liberar
  3. Realización de pedidos: cómo: Mutex, Atomics y / o Fences
  4. Otras restricciones sobre compiladores y hardware
  5. Generación de código y rendimiento: 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ó semántica relajada al estándar solo porque POWER y ARM no admiten la carga sincronizada de manera eficiente?).

respondido 06 nov., 17:02

Esa charla es realmente fantástica, vale la pena las 3 horas que pasará viéndola. - ZunTzu

@ZunTzu: en la mayoría de los reproductores de video, puede establecer la velocidad en 1.25, 1.5 o incluso 2 veces la original. - Christian Severin

@eran, chicos, ¿tenéis las diapositivas? los enlaces en las páginas de discusión del canal 9 no funcionan. - Athos

@athos no los tengo, lo siento. Intente ponerse en contacto con el canal 9, no creo que la eliminación fue 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

Significa que el estándar ahora define multiproceso y define lo que sucede en el contexto de múltiples subprocesos. Por supuesto, la gente usó diferentes implementaciones, pero eso es como preguntar por qué deberíamos tener una std::string cuando todos podríamos estar usando un enrollado en casa string clase.

Cuando habla de subprocesos POSIX o subprocesos de Windows, entonces esto es un poco ilusorio, ya que en realidad está hablando de subprocesos x86, ya que es una función de hardware que se ejecuta al mismo tiempo. El modelo de memoria C ++ 0x ofrece garantías, ya sea que esté en x86, ARM o MIPS, o cualquier otra cosa que se te ocurra.

respondido 06 nov., 17:02

Los subprocesos 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 subprocesos 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 aparecen en implementaciones de subprocesos de hardware (y algunos incluso solo en sistemas multiprocesador / multinúcleo). - celtschk

Para los 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 a la memoria para mejorar el rendimiento. Entonces, si su programa tiene carreras de datos (una carrera de datos es cuando es posible que varios núcleos / hiperprocesos accedan a la misma memoria al mismo tiempo), entonces su programa no es multiplataforma debido a su dependencia del modelo de memoria del procesador. Puede consultar los manuales de software de Intel o AMD para averiguar cómo los procesadores pueden reordenar los accesos a la memoria.

Es muy importante que los bloqueos (y la semántica de simultaneidad con bloqueo) se implementen típicamente de una manera 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 ++ han adquirido / liberado semántica para volatile, que es una extensión de C ++ para hacer frente a 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 se ejecuta solo 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 / liberación en un idioma).

respondido 06 nov., 17:02

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 en día se ejecuta en varias versiones de ARM, que es bastante diferente en cuanto a memoria de x86, y en ninguna parte tan indulgente. - Lorenzo Dematté

Ese vínculo está algo roto (dice "Documentación retirada de Visual Studio 2005"). ¿Quieres actualizarlo? - Peter Mortensen

No era cierto incluso cuando se escribió la respuesta. - Ben

"para acceder a la misma memoria al mismo tiempo"para acceder en un en conflicto camino - curioso

Si usa mutex para proteger todos sus datos, realmente no debería tener que preocuparse. Los mutex siempre han proporcionado suficientes garantías de orden y visibilidad.

Ahora bien, 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 proporcionan garantías de orden y visibilidad, y proporciona vallas portátiles para garantías codificadas a mano.

Anteriormente, la atomización se realizaba utilizando 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).

Respondido el 12 de junio de 11 a las 03:06

El problema anterior era que no existía un mutex (en términos del estándar C ++). Entonces, las únicas garantías que le brindó fueron las del fabricante de mutex, lo cual estuvo bien siempre que no transfiriera el código (ya que los cambios menores en las garantías son difíciles de detectar). Ahora tenemos las garantías proporcionadas por el estándar que deberían ser portátiles entre plataformas. - Martin York

@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

Además, mi punto fue principalmente que anteriormente no había un modelo de memoria a nivel de lenguaje, resultó ser el modelo de memoria de la CPU subyacente. Ahora hay un modelo de memoria que es parte del lenguaje central; OTOH, mutex y similares siempre se pueden hacer como una biblioteca. - ninjalj

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 violento desacuerdo sobre cómo se supone que funcionan estas cosas, bueno, a veces las cosas Los programadores de sistemas tenemos que hacer para presentar una bonita fachada a nivel de aplicaciones que no es nada agradable. - zwol

Desafortunadamente, no es suficiente proteger sus estructuras de datos con simples mutex si no hay un modelo de memoria consistente en su lenguaje. Hay varias optimizaciones del compilador que tienen sentido en un contexto de un solo subproceso, pero cuando entran en juego varios subprocesos y núcleos de CPU, el reordenamiento de los accesos a la memoria y otras optimizaciones pueden producir un comportamiento indefinido. Para obtener más información, consulte "Los subprocesos no se pueden implementar como una biblioteca" por Hans Boehm: citeseer.ist.psu.edu/viewdoc/… - exDM69

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

Hay un lugar donde los errores aún son 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 bloqueada. 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, data1 y data2 tenga que ser atomic<>. Si no son atómicos, entonces podrían leerse (en reader()) en el mismo momento exacto en que se escriben (en writer()). Según el modelo de memoria de C ++, esta es una carrera aunque reader() en realidad nunca usa 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ías eso ... quieres volver a leer en cada iteración del while incluyete 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 (en reader()) solo tiene adquirir semántica. En términos simples, si X e Y son accesos a la memoria, X precede a Y, X no es una adquisición o liberación, y Y es una adquisición, entonces el compilador puede reordenar Y antes que X. Si Y fue la segunda lectura de la secuencia, y X fue una lectura de datos, tal reordenamiento rompería la implementación de bloqueo.

El documento da algunas soluciones. El que tiene el mejor rendimiento en la actualidad es probablemente el que usa un atomic_thread_fence con 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í debería leer el documento. Es más preciso y completo que este post.

El último problema es que podría ser antinatural hacer que el data variables 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 tipos primitivos. Se supone que C ++ 20 agrega atomic_ref<>, lo que facilitará la resolución de este problema.

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

Respondido el 20 de diciembre de 19 a las 06:12

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

Ahora están mitad definidos por una traza de ejecución de un programa y mitad a posteriori por muchos ordenamientos en objetos de sincronización.

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

Respondido 28 Jul 19, 23:07

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 principios específicos del diseño del lenguaje. Después de eso, le recomiendo encarecidamente, si me lo permite, que dé en esa respuesta una muy buena argumentación sobre la relevancia de cada uno de esos puntos, porque se contrastarán con la relevancia de los inmensos beneficios de productividad percibidos por el diseño C ++ - Matias Haeussler

@MatiasHaeussler Creo que leíste mal mi respuesta; No me opongo a la definición de una característica de C ++ en particular aquí (también tengo muchas críticas, pero no aquí). Estoy argumentando aquí que no hay una construcción bien definida en C ++ (ni en C). Toda la semántica de 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 estoy en lo cierto, te refieres 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 enorme importancia para el futuro de la programación informática, 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 sean conscientes, e incluso después de comprender lo que quiere decir y de interesarse: Matias Haeussler

"¿Deberían publicarse las preguntas sobre la demostrabilidad de los programas de computadora en stackoverflow o en stackexchange (si no está en ninguno, dónde)?" Este parece ser uno para meta stackoverflow, ¿no es así? - Matias Haeussler

@MatiasHaeussler 1) C y C ++ comparten esencialmente el "modelo de memoria" de variables atómicas, mutex y multiproceso. 2) La relevancia de esto tiene que ver con los beneficios de tener el "modelo de memoria". Creo que el beneficio es cero ya que el modelo no es sólido. - curioso

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.