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 claseconcurrent_vector
no proporciona el métodoinsert
.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étodoserase
nipop_back
. Al igual que convector
, use el método clear para quitar todos los elementos de un objetoconcurrent_vector
.La clase
concurrent_vector
no almacena sus elementos de forma contigua en memoria. Por lo tanto, no puede usar la claseconcurrent_vector
de todas las formas en las que se puede usar una matriz. Por ejemplo, para una variable llamadav
de tipoconcurrent_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 tipobool
.
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étodosfront
nipop
. La claseconcurrent_queue
reemplaza estos métodos definiendo el método try_pop.La clase
concurrent_queue
no proporciona el métodoback
. 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étodosize
. El métodounsafe_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
ybucket_size
se llamanunsafe_erase
,unsafe_bucket
,unsafe_bucket_count
yunsafe_bucket_size
, respectivamente. La convención de nomenclaturaunsafe_
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 proporcionaoperator[]
ni el métodoat
.
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 proporcionaoperator[]
ni el métodoat
.
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.
Method | 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:
Procedimiento para usar la clase combinable para mejorar el rendimiento
Procedimiento para usar la clase combinable para combinar conjuntos
[Arriba]
Temas relacionados
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_unordered_map (clase)
concurrent_unordered_multimap (clase)
concurrent_unordered_set (clase)