Buscar..


Observaciones

Los diferentes subprocesos que intentan acceder a la misma ubicación de memoria participan en una carrera de datos si al menos una de las operaciones es una modificación (también conocida como operación de almacenamiento ). Estas razas de datos causan un comportamiento indefinido . Para evitarlos, es necesario evitar que estos subprocesos ejecuten simultáneamente dichas operaciones en conflicto.

Las primitivas de sincronización (exclusión mutua, sección crítica y similares) pueden proteger tales accesos. El modelo de memoria introducido en C ++ 11 define dos nuevas formas portátiles de sincronizar el acceso a la memoria en un entorno de múltiples subprocesos: operaciones atómicas y cercas.

Operaciones atómicas

Ahora es posible leer y escribir en una ubicación de memoria determinada mediante el uso de carga atómica y operaciones de almacenamiento atómico . Para mayor comodidad, estos se incluyen en la clase de plantilla std::atomic<t> . Esta clase ajusta un valor de tipo t pero esta vez se carga y se almacena en el objeto atómico.

La plantilla no está disponible para todos los tipos. Los tipos disponibles son específicos de la implementación, pero esto generalmente incluye la mayoría (o todos) los tipos integrales disponibles, así como los tipos de punteros. Así que std::atomic<unsigned> y std::atomic<std::vector<foo> *> deberían estar disponibles, mientras que std::atomic<std::pair<bool,char>> probablemente no estará disponible.

Las operaciones atómicas tienen las siguientes propiedades:

  • Todas las operaciones atómicas se pueden realizar simultáneamente desde varios subprocesos sin causar un comportamiento indefinido.
  • Una carga atómica verá el valor inicial con el que se construyó el objeto atómico o el valor que se le escribió a través de alguna operación de almacenamiento atómico .
  • Las tiendas atómicas para el mismo objeto atómico se ordenan de la misma manera en todos los hilos. Si una hebra ya ha visto el valor de alguna operación de almacenamiento atómico , las operaciones de carga atómica subsiguientes verán el mismo valor o el valor almacenado por la operación de almacenamiento atómico subsiguiente.
  • Las operaciones atómicas de lectura-modificación-escritura permiten que la carga atómica y el almacenamiento atómico ocurran sin otro almacenamiento atómico en el medio. Por ejemplo, uno puede incrementar atómicamente un contador desde varios subprocesos, y no se perderá ningún incremento, independientemente de la contención entre los subprocesos.
  • Las operaciones atómicas reciben un parámetro opcional std::memory_order que define qué propiedades adicionales tiene la operación con respecto a otras ubicaciones de memoria.
std :: memory_order Sentido
std::memory_order_relaxed sin restricciones adicionales
std::memory_order_releasestd::memory_order_acquire si load-acquire ve el valor almacenado por store-release entonces los almacenes secuenciados antes store-release ocurra el store-release antes de las cargas secuenciadas después de la adquisición de la carga
std::memory_order_consume como memory_order_acquire pero solo para cargas dependientes
std::memory_order_acq_rel Combina load-acquire y store-release
std::memory_order_seq_cst consistencia secuencial

Estas etiquetas de orden de memoria permiten tres disciplinas de ordenación de memoria diferentes: coherencia secuencial , relajada y adquisición de versión con su versión de consumo de hermanos.

Consistencia secuencial

Si no se especifica ningún orden de memoria para una operación atómica, el orden se establece de manera predeterminada en consistencia secuencial . Este modo también se puede seleccionar explícitamente etiquetando la operación con std::memory_order_seq_cst .

Con este orden, ninguna operación de memoria puede cruzar la operación atómica. Todas las operaciones de memoria secuenciadas antes de la operación atómica suceden antes de la operación atómica y la operación atómica ocurre antes de todas las operaciones de memoria que se secuencian después de esta. Este modo es probablemente el más fácil de razonar, pero también conduce a la mayor penalización para el rendimiento. También evita todas las optimizaciones del compilador que de otro modo podrían intentar reordenar las operaciones después de la operación atómica.

Pedidos relajados

Lo opuesto a la consistencia secuencial es el ordenamiento de memoria relajado . Se selecciona con la etiqueta std::memory_order_relaxed . La operación atómica relajada no impondrá restricciones en otras operaciones de memoria. El único efecto que queda, es que la operación sigue siendo atómica.

Liberar-Adquirir pedidos

Una operación de almacenamiento atómico se puede etiquetar con std::memory_order_release y una operación de carga atómica se puede etiquetar con std::memory_order_acquire . La primera operación se llama liberación atómica (store-release), mientras que la segunda se llama adquisición (carga atómica) .

Cuando la adquisición de carga ve el valor escrito por un lanzamiento de tienda, sucede lo siguiente: todas las operaciones de almacenamiento secuenciadas antes de la liberación de almacenamiento se vuelven visibles ( suceden antes ) las operaciones de carga que se secuencian después de la adquisición de carga .

Las operaciones atómicas de lectura-modificación-escritura también pueden recibir la etiqueta acumulativa std::memory_order_acq_rel . Esto hace que la porción de carga atómica de la operación sea una carga atómica adquirida, mientras que la porción de almacenamiento atómico se convierte en liberación de almacenamiento atómico .

Al compilador no se le permite mover las operaciones de la tienda después de una operación de liberación atómica de la tienda . Tampoco está permitido mover las operaciones de carga antes de la carga atómica de adquisición (o carga de consumo ).

También tenga en cuenta que no hay liberación de carga atómica o adquisición de almacén atómica . Intentar crear tales operaciones hace que sean operaciones relajadas .

Orden de liberación de consumo

Esta combinación es similar a la adquisición por versión , pero esta vez la carga atómica se etiqueta con std::memory_order_consume y se convierte en una std::memory_order_consume (atómica) de consumo de carga . Este modo es el mismo que el de adquisición de versión, con la única diferencia de que entre las operaciones de carga secuenciadas después de la carga y el consumo, solo en función del valor cargado por la carga y el consumo se ordenan.

Vallas

Las cercas también permiten que las operaciones de memoria se ordenen entre hilos. Una valla es una valla de liberación o adquirir una valla.

Si una valla de liberación ocurre antes de una cerca de adquisición, entonces las tiendas secuenciadas antes de la cerca de liberación son visibles para las cargas secuenciadas después de la cerca de adquisición. Para garantizar que el cerco de liberación ocurra antes del cercado de adquisición, se pueden usar otras primitivas de sincronización, incluidas las operaciones atómicas relajadas.

Necesidad de modelo de memoria

int x, y;
bool ready = false;

void init()
{
  x = 2;
  y = 3;
  ready = true;
}
void use()
{
  if (ready)
    std::cout << x + y;
}

Un subproceso llama a la función init() mientras que otro subproceso (o controlador de señales) llama a la función use() . Uno podría esperar que la función use() imprima 5 o no haga nada. Esto puede no ser siempre el caso por varias razones:

  • La CPU puede reordenar las escrituras que ocurren en init() para que el código que realmente se ejecuta pueda tener el siguiente aspecto:

    void init()
    {
      ready = true;
      x = 2;
      y = 3;
    }
    
  • La CPU puede reordenar las lecturas que ocurren en use() para que el código realmente ejecutado se convierta en:

    void use()
    {
      int local_x = x;
      int local_y = y;
      if (ready)
        std::cout << local_x + local_y;
    }
    
  • Un compilador optimizado de C ++ puede decidir reordenar el programa de manera similar.

Tal reordenación no puede cambiar el comportamiento de un programa que se ejecuta en un solo hilo porque un hilo no puede intercalar las llamadas a init() y use() . Por otro lado, en una configuración de subprocesos múltiples, un subproceso puede ver parte de las escrituras realizadas por el otro subproceso en las que puede suceder que use() vea ready==true y basura en x o y o ambos.

El modelo de memoria C ++ permite que el programador especifique qué operaciones de reordenación están permitidas y cuáles no, de modo que un programa de subprocesos múltiples también podría comportarse como se espera. El ejemplo anterior se puede reescribir de forma segura para subprocesos como este:

int x, y;
std::atomic<bool> ready{false};

void init()
{
  x = 2;
  y = 3;
  ready.store(true, std::memory_order_release);
}
void use()
{
  if (ready.load(std::memory_order_acquire))
    std::cout << x + y;
}

Aquí init() realiza una operación de liberación de tienda atómica . Esto no solo almacena el valor true en ready , sino que también le dice al compilador que no puede mover esta operación antes de las operaciones de escritura que se secuencian antes .

La función use() realiza una operación de adquisición de carga atómica . Lee el valor actual de ready y también prohíbe al compilador colocar las operaciones de lectura que se secuencian después de que suceda antes de la carga atómica .

Estas operaciones atómicas también hacen que el compilador ponga todas las instrucciones de hardware necesarias para informar a la CPU que se abstenga de realizar reordenamientos no deseados.

Debido a que el lanzamiento de la tienda atómica se encuentra en la misma ubicación de memoria que la adquisición de carga atómica , el modelo de memoria estipula que si la operación de adquisición de la carga ve el valor escrito por la operación de lanzamiento de la tienda , entonces todas las escrituras realizadas por init() ' El subproceso s antes de ese lanzamiento de tienda será visible para las cargas que use() el subproceso de use() ejecuta después de su adquisición de carga . Es decir, si use() ve ready==true , entonces se garantiza que veamos x==2 y y==3 .

Tenga en cuenta que el compilador y la CPU todavía pueden escribir en y antes de escribir en x , y de manera similar, las lecturas de estas variables en use() pueden suceder en cualquier orden.

Ejemplo de valla

El ejemplo anterior también se puede implementar con cercas y operaciones atómicas relajadas:

int x, y;
std::atomic<bool> ready{false};

void init()
{
  x = 2;
  y = 3;
  atomic_thread_fence(std::memory_order_release);
  ready.store(true, std::memory_order_relaxed);
}
void use()
{
  if (ready.load(std::memory_order_relaxed))
  {
    atomic_thread_fence(std::memory_order_acquire);
    std::cout << x + y;
  }
}

Si la operación de carga atómica ve el valor escrito por el almacén atómico, entonces el almacenamiento ocurre antes de la carga, y también lo hacen las cercas: la cerca de liberación ocurre antes de la cerca de adquisición, haciendo que las escrituras a x e y que preceden a la cerca de liberación se hagan visibles. a la instrucción std::cout que sigue a la valla de adquisición.

Una valla podría ser beneficiosa si puede reducir el número total de operaciones de adquisición, liberación u otras operaciones de sincronización. Por ejemplo:

void block_and_use()
{
  while (!ready.load(std::memory_order_relaxed))
    ;
  atomic_thread_fence(std::memory_order_acquire);
  std::cout << x + y;
}

La función block_and_use() gira hasta que la block_and_use() ready se establece con la ayuda de carga atómica relajada. Luego, se utiliza una sola guía de adquisición para proporcionar el orden de memoria necesario.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow