Contenedores y objetos paralelos

La biblioteca de patrones de procesamiento paralelo (PPL) incluye varios contenedores y objetos que proporcionan acceso seguro para subprocesos a sus elementos.

Un contenedor simultáneo proporciona acceso seguro para la simultaneidad a las operaciones más importantes. Aquí, seguro para la simultaneidad significa que los punteros e iteradores siempre son válidos. No es una garantía de inicialización de elementos ni de un orden transversal determinado. La funcionalidad de estos contenedores es similar a la proporcionada por la biblioteca estándar de C++. Por ejemplo, la clase concurrency::concurrent_vector es similar a la clase std::vector, salvo que la clase concurrent_vector permite anexar elementos en paralelo. Use contenedores simultáneos cuando tenga código paralelo que requiera acceso de lectura y escritura al mismo contenedor.

Un objeto simultáneo se comparte simultáneamente entre componentes. Un proceso que calcula el estado de un objeto simultáneo en paralelo genera el mismo resultado que otro proceso que calcula el mismo estado en serie. La clase concurrency::combinable es un ejemplo de un tipo de objeto simultáneo. La clase combinable permite realizar cálculos en paralelo y, después, combinar esos cálculos en un resultado final. Use objetos simultáneos cuando, de lo contrario, usaría un mecanismo de sincronización; por ejemplo, una exclusión mutua, para sincronizar el acceso a una variable o un recurso compartido.

Secciones

En este tema, se describen en detalle los siguientes contenedores y objetos paralelos.

Contenedores simultáneos:

Objetos simultáneos:

Clase concurrent_vector

La clase concurrency::concurrent_vector es una clase contenedora de secuencias que, al igual que la clase std::vector, permite acceder aleatoriamente a sus elementos. La clase concurrent_vector habilita operaciones de anexión y acceso a elementos seguras para la simultaneidad. Las operaciones de anexión no invalidan los punteros e iteradores existentes. Las operaciones de acceso y recorrido de iterador también son seguras para la simultaneidad. Aquí, seguro para la simultaneidad significa que los punteros e iteradores siempre son válidos. No es una garantía de inicialización de elementos ni de un orden transversal determinado.

Diferencias entre concurrent_vector y vector

La clase concurrent_vector se parece mucho a la clase vector. La complejidad de las operaciones de anexión, acceso a elementos e iteradores en un objeto concurrent_vector es la misma que para un objeto vector. Los siguientes puntos muestran dónde difiere concurrent_vector de vector:

  • Las operaciones de anexión, acceso a elementos y recorrido de iteradores en un objeto concurrent_vector son seguras para la simultaneidad.

  • Solo puede agregar elementos al final de un objeto concurrent_vector. La clase concurrent_vector no proporciona el método insert.

  • Un objeto concurrent_vector no usa la semántica de transferencia de recursos cuando le anexa un elemento.

  • La clase concurrent_vector no proporciona los métodos erase ni pop_back. Al igual que con vector, use el método clear para quitar todos los elementos de un objeto concurrent_vector.

  • La clase concurrent_vector no almacena sus elementos de forma contigua en memoria. Por lo tanto, no puede usar la clase concurrent_vector de todas las formas en las que se puede usar una matriz. Por ejemplo, para una variable llamada v de tipo concurrent_vector, la expresión &v[0]+2 genera un comportamiento indefinido.

  • La clase concurrent_vector define los métodos grow_by y grow_to_at_least. Estos métodos son similares al método resize, salvo que son seguros para la simultaneidad.

  • Un objeto concurrent_vector no reubica sus elementos al realizar una anexión o cambiar su tamaño. Esto permite que los punteros e iteradores existentes sigan siendo válidos durante las operaciones simultáneas.

  • El entorno de ejecución no define una versión especializada de concurrent_vector para el tipo bool.

Operaciones seguras para la simultaneidad

Todos los métodos que anexan o aumentan el tamaño de un objeto concurrent_vector, o que acceden a un elemento de un objeto concurrent_vector, son seguros para la simultaneidad. Aquí, seguro para la simultaneidad significa que los punteros e iteradores siempre son válidos. No es una garantía de inicialización de elementos ni de un orden transversal determinado. El método resize es la excepción a esta regla.

En la tabla siguiente, se muestran los métodos y operadores comunes de concurrent_vector que son seguros para la simultaneidad.

Las operaciones que proporciona el entorno de ejecución para la compatibilidad con la biblioteca estándar de C++, por ejemplo, reserve, no son seguras para simultaneidad. En la tabla siguiente, se muestran los métodos y operadores comunes que no son seguros para la simultaneidad.

Las operaciones que modifican el valor de los elementos existentes no son seguras para la simultaneidad. Use un objeto de sincronización, como un objeto reader_writer_lock, para sincronizar las operaciones simultáneas de lectura y escritura en el mismo elemento de datos. Para obtener más información sobre los objetos de sincronización, consulte Estructuras de datos de sincronización.

Al convertir código existente que usa vector para usar concurrent_vector, las operaciones simultáneas pueden provocar que cambie el comportamiento de la aplicación. Por ejemplo, considere el siguiente programa que realiza simultáneamente dos tareas en un objeto concurrent_vector. La primera tarea anexa elementos adicionales a un objeto concurrent_vector. La segunda tarea calcula la suma de todos los elementos del mismo objeto.

// parallel-vector-sum.cpp
// compile with: /EHsc
#include <ppl.h>
#include <concurrent_vector.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
   // Create a concurrent_vector object that contains a few
   // initial elements.
   concurrent_vector<int> v;
   v.push_back(2);
   v.push_back(3);
   v.push_back(4);
   
   // Perform two tasks in parallel.
   // The first task appends additional elements to the concurrent_vector object.
   // The second task computes the sum of all elements in the same object.

   parallel_invoke(
      [&v] { 
         for(int i = 0; i < 10000; ++i)
         {
            v.push_back(i);
         }
      },
      [&v] {
         combinable<int> sums;
         for(auto i = begin(v); i != end(v); ++i) 
         {
            sums.local() += *i;
         }     
         wcout << L"sum = " << sums.combine(plus<int>()) << endl;
      }
   );
}

Aunque el método end es seguro para simultaneidad, una llamada simultánea al método push_back hace que cambie el valor devuelto por end. El número de elementos que recorre el iterador es indeterminado. Por lo tanto, este programa puede generar un resultado diferente cada vez que lo ejecute. Cuando el tipo de elemento no es trivial, es posible que exista una condición de carrera entre las llamadas a push_back y end. El método end puede devolver un elemento que está asignado, pero no está completamente inicializado.

Seguridad de las excepciones

Si una operación de crecimiento o asignación produce una excepción, el estado del objeto concurrent_vector deja de ser válido. El comportamiento de un concurrent_vector objeto que está en un estado no válido no está definido a menos que se indique lo contrario. Sin embargo, el destructor siempre libera la memoria que asigna el objeto, incluso si el objeto está en un estado no válido.

El tipo de datos de los elementos del vector, T, debe cumplir los siguientes requisitos. De lo contrario, el comportamiento de la clase concurrent_vector es indefinido.

  • El destructor no debe iniciarse.

  • Si se inicia el constructor de copia o predeterminado, el destructor no se debe declarar mediante la palabra clave virtual y debe funcionar correctamente con memoria inicializada en cero.

[Arriba]

Clase concurrent_queue

La clase concurrency::concurrent_queue, al igual que la clase std::queue, le permite acceder a sus elementos frontal y trasero. La clase concurrent_queue habilita operaciones de puesta en cola y retirada de la cola seguras para simultaneidad. Aquí, seguro para la simultaneidad significa que los punteros e iteradores siempre son válidos. No es una garantía de inicialización de elementos ni de un orden transversal determinado. La clase concurrent_queue también proporciona compatibilidad con iteradores que no sean seguros para la simultaneidad.

Diferencias entre concurrent_queue y queue

La clase concurrent_queue se parece mucho a la clase queue. Los siguientes puntos muestran dónde difiere concurrent_queue de queue:

  • Las operaciones de puesta en cola y de retirada de la cola en un objeto concurrent_queue son seguras para la simultaneidad.

  • La clase concurrent_queue proporciona compatibilidad con iteradores que no sean seguros para la simultaneidad.

  • La clase concurrent_queue no proporciona los métodos front ni pop. La clase concurrent_queue reemplaza estos métodos definiendo el método try_pop.

  • La clase concurrent_queue no proporciona el método back. Por lo tanto, no puede hacer referencia al final de la cola.

  • La clase concurrent_queue proporciona el método unsafe_size en lugar del método size. El método unsafe_size no es seguro para la simultaneidad.

Operaciones seguras para la simultaneidad

Todos los métodos que ponen en cola o retiran de la cola de un objeto concurrent_queue son seguros para la simultaneidad. Aquí, seguro para la simultaneidad significa que los punteros e iteradores siempre son válidos. No es una garantía de inicialización de elementos ni de un orden transversal determinado.

En la tabla siguiente, se muestran los métodos y operadores comunes de concurrent_queue que son seguros para la simultaneidad.

Aunque el método empty es seguro para la simultaneidad, una operación simultánea puede hacer que la cola crezca o se reduzca antes de que vuelva el método empty.

En la tabla siguiente, se muestran los métodos y operadores comunes que no son seguros para la simultaneidad.

Compatibilidad de iteradores

concurrent_queue proporciona iteradores que no son seguros para la simultaneidad. Se recomienda usar estos iteradores solo para la depuración.

Un iterador de concurrent_queue recorre los elementos solo en la dirección hacia delante. En la tabla siguiente, se muestran los operadores que admite cada iterador.

Operator Descripción
operator++ Avanza al siguiente elemento de la cola. Este operador está sobrecargado para proporcionar semántica previa y posterior al incremento.
operator* Recupera una referencia al elemento actual.
operator-> Recupera un puntero al elemento actual.

[Arriba]

concurrent_unordered_map (Clase)

La clase concurrency::concurrent_unordered_map es una clase contenedora asociativa que, al igual que la clase std::unordered_map, controla una secuencia de longitud variable de elementos de tipo std::pair<const Key, Ty>. Piense en un mapa sin ordenar como un diccionario al que puede agregar un par clave-valor a o buscar un valor por clave. Esta clase es útil cuando tiene varios subprocesos o tareas que tienen que acceder simultáneamente a un contenedor compartido, realizar inserciones o realizar actualizaciones.

En el siguiente ejemplo, se muestra la estructura básica del uso de concurrent_unordered_map. En este ejemplo, se insertan claves de carácter en el intervalo ['a', 'i']. Dado que el orden de las operaciones es indeterminado, el valor final de cada clave también es indeterminado. Sin embargo, es seguro realizar las inserciones en paralelo.

// unordered-map-structure.cpp
// compile with: /EHsc
#include <ppl.h>
#include <concurrent_unordered_map.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain() 
{
    //
    // Insert a number of items into the map in parallel.

    concurrent_unordered_map<char, int> map; 

    parallel_for(0, 1000, [&map](int i) {
        char key = 'a' + (i%9); // Geneate a key in the range [a,i].
        int value = i;          // Set the value to i.
        map.insert(make_pair(key, value));
    });

    // Print the elements in the map.
    for_each(begin(map), end(map), [](const pair<char, int>& pr) {
        wcout << L"[" << pr.first << L", " << pr.second << L"] ";
    });
}
/* Sample output:
    [e, 751] [i, 755] [a, 756] [c, 758] [g, 753] [f, 752] [b, 757] [d, 750] [h, 754]
*/

Para ver un ejemplo que usa concurrent_unordered_map para realizar una operación de asignación y reducción en paralelo, consulte Procedimientos: Realización de operaciones de asignación y reducción en paralelo.

Diferencias entre concurrent_unordered_map y unordered_map

La clase concurrent_unordered_map se parece mucho a la clase unordered_map. Los siguientes puntos muestran dónde difiere concurrent_unordered_map de unordered_map:

  • Los métodos erase, bucket, bucket_count y bucket_size se llaman unsafe_erase, unsafe_bucket, unsafe_bucket_count y unsafe_bucket_size, respectivamente. La convención de nomenclatura unsafe_ indica que estos métodos no son seguros para la simultaneidad. Para obtener más información sobre la seguridad de la simultaneidad, consulte Operaciones seguras para la simultaneidad.

  • Las operaciones de inserción no invalidan los punteros e iteradores existentes, ni cambian el orden de los elementos que ya existen en el mapa. Las operaciones de inserción y recorrido se pueden producir simultáneamente.

  • concurrent_unordered_map admite solo la iteración hacia delante.

  • La inserción no invalida ni actualiza los iteradores devueltos por equal_range. La inserción puede anexar elementos desiguales al final del intervalo. El iterador begin apunta a un elemento igual.

Para evitar los interbloqueos, ningún método de concurrent_unordered_map contiene un bloqueo cuando llama al asignador de memoria, las funciones hash u otro código definido por el usuario. Además, debe asegurarse de que la función hash evalúe siempre las claves iguales como el mismo valor. Las mejores funciones hash distribuyen las claves uniformemente en el espacio de código hash.

Operaciones seguras para la simultaneidad

La clase concurrent_unordered_map habilita operaciones de inserción y acceso a elementos seguras para la simultaneidad. Las operaciones de inserción no invalidan los punteros e iteradores existentes. Las operaciones de acceso y recorrido de iterador también son seguras para la simultaneidad. Aquí, seguro para la simultaneidad significa que los punteros e iteradores siempre son válidos. No es una garantía de inicialización de elementos ni de un orden transversal determinado. En la tabla siguiente, se muestran los métodos y operadores de concurrent_unordered_map utilizados habitualmente que son seguros para la simultaneidad.

Aunque se puede llamar al método count de forma segura desde subprocesos en ejecución simultánea, los distintos subprocesos pueden recibir resultados diferentes si se inserta simultáneamente un nuevo valor en el contenedor.

En la tabla siguiente, se muestran los métodos y operadores utilizados habitualmente que no son seguros para la simultaneidad.

Además de estos métodos, cualquier método que comience por unsafe_ tampoco es seguro para la simultaneidad.

[Arriba]

concurrent_unordered_multimap (Clase)

La clase concurrency::concurrent_unordered_multimap se parece mucho a la clase concurrent_unordered_map, salvo que permite que se asignen varios valores a la misma clave. También difiere de concurrent_unordered_map de las maneras siguientes:

  • El método concurrent_unordered_multimap::insert devuelve un iterador en lugar de std::pair<iterator, bool>.

  • La clase concurrent_unordered_multimap no proporciona operator[] ni el método at.

En el siguiente ejemplo, se muestra la estructura básica del uso de concurrent_unordered_multimap. En este ejemplo, se insertan claves de carácter en el intervalo ['a', 'i']. concurrent_unordered_multimap permite que una clave tenga varios valores.

// unordered-multimap-structure.cpp
// compile with: /EHsc
#include <ppl.h>
#include <concurrent_unordered_map.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain() 
{
    //
    // Insert a number of items into the map in parallel.

    concurrent_unordered_multimap<char, int> map; 

    parallel_for(0, 10, [&map](int i) {
        char key = 'a' + (i%9); // Geneate a key in the range [a,i].
        int value = i;          // Set the value to i.
        map.insert(make_pair(key, value));
    });

    // Print the elements in the map.
    for_each(begin(map), end(map), [](const pair<char, int>& pr) {
        wcout << L"[" << pr.first << L", " << pr.second << L"] ";
    });
}
/* Sample output:
    [e, 4] [i, 8] [a, 9] [a, 0] [c, 2] [g, 6] [f, 5] [b, 1] [d, 3] [h, 7]
*/

[Arriba]

concurrent_unordered_set (Clase)

La clase concurrency::concurrent_unordered_set se parece mucho a la clase concurrent_unordered_map, salvo que administra valores en lugar de pares clave-valor. La clase concurrent_unordered_set no proporciona operator[] ni el método at.

En el siguiente ejemplo, se muestra la estructura básica del uso de concurrent_unordered_set. En este ejemplo, se insertan valores de carácter en el intervalo ['a', 'i']. Es seguro realizar las inserciones en paralelo.

// unordered-set-structure.cpp
// compile with: /EHsc
#include <ppl.h>
#include <concurrent_unordered_set.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain() 
{
    //
    // Insert a number of items into the set in parallel.

    concurrent_unordered_set<char> set; 

    parallel_for(0, 10000, [&set](int i) {
        set.insert('a' + (i%9)); // Geneate a value in the range [a,i].
    });

    // Print the elements in the set.
    for_each(begin(set), end(set), [](char c) {
        wcout << L"[" << c << L"] ";
    });
}
/* Sample output:
    [e] [i] [a] [c] [g] [f] [b] [d] [h]
*/

[Arriba]

concurrent_unordered_multiset (Clase)

La clase concurrency::concurrent_unordered_multiset se parece mucho a la clase concurrent_unordered_set, salvo que permite valores duplicados. También difiere de concurrent_unordered_set de las maneras siguientes:

  • El método concurrent_unordered_multiset::insert devuelve un iterador en lugar de std::pair<iterator, bool>.

  • La clase concurrent_unordered_multiset no proporciona operator[] ni el método at.

En el siguiente ejemplo, se muestra la estructura básica del uso de concurrent_unordered_multiset. En este ejemplo, se insertan valores de carácter en el intervalo ['a', 'i']. concurrent_unordered_multiset permite que aparezca un valor varias veces.

// unordered-set-structure.cpp
// compile with: /EHsc
#include <ppl.h>
#include <concurrent_unordered_set.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain() 
{
    //
    // Insert a number of items into the set in parallel.

    concurrent_unordered_multiset<char> set; 

    parallel_for(0, 40, [&set](int i) {
        set.insert('a' + (i%9)); // Geneate a value in the range [a,i].
    });

    // Print the elements in the set.
    for_each(begin(set), end(set), [](char c) {
        wcout << L"[" << c << L"] ";
    });
}
/* Sample output:
    [e] [e] [e] [e] [i] [i] [i] [i] [a] [a] [a] [a] [a] [c] [c] [c] [c] [c] [g] [g]
    [g] [g] [f] [f] [f] [f] [b] [b] [b] [b] [b] [d] [d] [d] [d] [d] [h] [h] [h] [h]
*/

[Arriba]

Clase combinable

La clase concurrency::combinable proporciona almacenamiento reutilizable y local para los subprocesos que permite realizar cálculos pormenorizados y, a continuación, combinar estos cálculos en un resultado final. Puede pensar en un objeto combinable como una variable de reducción.

La clase combinable es útil cuando tiene un recurso que se comparte entre varios subprocesos o tareas. La clase combinable ayuda a eliminar el estado compartido al proporcionar acceso a los recursos compartidos sin utilizar bloqueos. Por lo tanto, esta clase proporciona una alternativa al uso de un mecanismo de sincronización, por ejemplo, una exclusión mutua, para sincronizar el acceso a datos compartidos desde varios subprocesos.

Métodos y características

En la tabla siguiente, se muestran algunos de los métodos importantes de la clase combinable. Para obtener información sobre todos los métodos de la clase combinable, consulte la clase combinable.

Método Descripción
local Recupera una referencia a la variable local asociada al contexto del subproceso actual.
eliminar Quita todas las variables locales del subproceso del objeto combinable.
combine

combine_each
Usa la función de combinación proporcionada para generar un valor final a partir del conjunto de todos los cálculos locales del subproceso.

La clase combinable es una clase de plantilla parametrizada en el resultado combinado final. Si llama al constructor predeterminado, el tipo de parámetro de plantilla T debe tener un constructor predeterminado y un constructor de copia. Si el tipo de parámetro de plantilla T no tiene un constructor predeterminado, llame a la versión sobrecargada del constructor, que toma una función de inicialización como parámetro.

Puede almacenar datos adicionales en un objeto combinable después de llamar a los métodos combine o combine_each. También puede llamar a los métodos combine y combine_each varias veces. Si no cambia ningún valor local en un objeto combinable, los métodos combine y combine_each generan el mismo resultado cada vez que se les llama.

Ejemplos

Para ver ejemplos sobre cómo usar la clase combinable, consulte los temas siguientes:

[Arriba]

Procedimiento para usar contenedores paralelos para aumentar la eficacia
Muestra cómo usar contenedores paralelos para almacenar los datos y tener acceso a ellos en paralelo de forma eficaz.

Procedimiento para usar la clase combinable para mejorar el rendimiento
Muestra cómo usar la clase combinable para eliminar el estado compartido y, por tanto, mejorar el rendimiento.

Procedimiento para usar la clase combinable para combinar conjuntos
Muestra cómo usar una función combine para combinar conjuntos de datos locales del subproceso.

Biblioteca de modelos de procesamiento paralelo (PPL)
Describe la biblioteca de patrones de procesamiento paralelo (PPL) la cual proporciona un modelo de programación imperativo que favorece la escalabilidad y facilidad de uso para desarrollar aplicaciones simultáneas.

Referencia

concurrent_vector (clase)

concurrent_queue (clase)

concurrent_unordered_map (clase)

concurrent_unordered_multimap (clase)

concurrent_unordered_set (clase)

concurrent_unordered_multiset (clase)

combinable (clase)