Partilhar via


Contêineres e objetos paralelos

A Biblioteca de Padrões Paralelos (PPL) inclui vários contentores e objetos que fornecem acesso seguro para threads aos seus elementos.

Um contêiner simultâneo fornece acesso seguro de simultaneidade às operações mais importantes. Aqui, simultaneidade segura significa que ponteiros ou iteradores são sempre válidos. Não é uma garantia de inicialização de elementos ou de uma ordem transversal específica. A funcionalidade desses contêineres é semelhante àquelas fornecidas pela Biblioteca Padrão C++. Por exemplo, a classe concurrency::concurrent_vetor é semelhante à classe std::vetor , exceto que a concurrent_vector classe permite acrescentar elementos em paralelo. Use contêineres simultâneos quando tiver um código paralelo que exija acesso de leitura e gravação ao mesmo contêiner.

Um objeto simultâneo é compartilhado simultaneamente entre componentes. Um processo que calcula o estado de um objeto simultâneo em paralelo produz o mesmo resultado que outro processo que calcula o mesmo estado em série. A classe concurrency::combinable é um exemplo de um tipo de objeto simultâneo. A combinable classe permite que você execute cálculos em paralelo e, em seguida, combine esses cálculos em um resultado final. Use objetos simultâneos quando você usaria um mecanismo de sincronização, por exemplo, um mutex, para sincronizar o acesso a uma variável ou recurso compartilhado.

Secções

Este tópico descreve os seguintes contêineres e objetos paralelos em detalhes.

Contentores simultâneos:

Objetos simultâneos:

concurrent_vetor Classe

A classe concurrency::concurrent_vetor é uma classe de contêiner de sequência que, assim como a classe std::vetor , permite que você acesse aleatoriamente seus elementos. A classe concurrent_vector permite operações de acréscimo e acesso a elementos seguros em ambientes de concorrência. As operações de acréscimo não invalidam ponteiros ou iteradores existentes. O acesso ao iterador e as operações de travessia também são seguros para simultaneidade. Aqui, simultaneidade segura significa que ponteiros ou iteradores são sempre válidos. Não é uma garantia de inicialização de elementos ou de uma ordem transversal específica.

Diferenças entre concurrent_vetor e vetor

A concurrent_vector classe se assemelha muito à vector classe. A complexidade das operações de acréscimo, acesso a elementos e acesso iterador em um concurrent_vector objeto é a mesma de um vector objeto. Os pontos a seguir ilustram onde concurrent_vector difere de vector:

  • As operações de acréscimo, acesso a elementos, acesso do iterador e iteração do iterador em um concurrent_vector objeto são seguras para acesso simultâneo.

  • Você pode adicionar elementos somente ao final de um concurrent_vector objeto. A concurrent_vector classe não fornece o insert método.

  • Um concurrent_vector objeto não usa semântica de movimentação quando você acrescenta a ele.

  • A concurrent_vector classe não fornece os erase métodos ou pop_back . Assim como no vector, use o método clear para remover todos os elementos de um concurrent_vector objeto.

  • A concurrent_vector classe não armazena seus elementos contíguos na memória. Portanto, você não pode usar a concurrent_vector classe de todas as maneiras que você pode usar uma matriz. Por exemplo, para uma variável chamada v do tipo concurrent_vector, a expressão &v[0]+2 produz um comportamento indefinido.

  • A concurrent_vector classe define os métodos grow_by e grow_to_at_least . Esses métodos se assemelham ao método de redimensionamento , exceto que eles são seguros para simultaneidade.

  • Um concurrent_vector objeto não realoca seus elementos quando você o acrescenta ou redimensiona. Isso permite que os ponteiros e iteradores existentes permaneçam válidos durante operações simultâneas.

  • O tempo de execução não define uma versão especializada para concurrent_vector do tipo bool.

Concurrency-Safe Operações

Todos os métodos que acrescentam ou aumentam o tamanho de um concurrent_vector objeto, ou acessam um elemento em um concurrent_vector objeto, são seguros para simultaneidade. Aqui, simultaneidade segura significa que ponteiros ou iteradores são sempre válidos. Não é uma garantia de inicialização de elementos ou de uma ordem transversal específica. A exceção a esta regra é o resize método.

A tabela a seguir mostra os métodos e operadores comuns concurrent_vector que são seguros para simultaneidade.

As operações que o tempo de execução fornece para compatibilidade com a Biblioteca Padrão C++, por exemplo, reservenão são seguras para simultaneidade. A tabela a seguir mostra os métodos e operadores comuns que não são seguros para simultaneidade.

As operações que modificam o valor dos elementos existentes não são seguras para simultaneidade. Use um objeto de sincronização, como um objeto reader_writer_lock , para sincronizar operações simultâneas de leitura e gravação com o mesmo elemento de dados. Para obter mais informações sobre objetos de sincronização, consulte Estruturas de dados de sincronização.

Quando se converte o código existente que usa vector para utilizar concurrent_vector, operações simultâneas podem alterar o comportamento da sua aplicação. Por exemplo, considere o seguinte programa que executa simultaneamente duas tarefas em um concurrent_vector objeto. A primeira tarefa acrescenta elementos adicionais a um concurrent_vector objeto. A segunda tarefa calcula a soma de todos os elementos no mesmo 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;
      }
   );
}

Embora o end método seja seguro para simultaneidade, uma chamada simultânea para o método push_back faz com que o valor retornado por end seja alterado. O número de elementos que o iterador percorre é indeterminado. Portanto, este programa pode produzir um resultado diferente cada vez que você executá-lo. Quando o tipo de elemento não é trivial, é possível que exista uma condição de corrida entre chamadas push_back e end. O end método pode retornar um elemento que está alocado, mas não totalmente inicializado.

Segurança de exceção

Se uma operação de crescimento ou atribuição lançar uma exceção, o estado do concurrent_vector objeto se tornará inválido. O comportamento de um concurrent_vector objeto que está em um estado inválido é indefinido, a menos que indicado de outra forma. No entanto, o destruidor sempre libera a memória que o objeto aloca, mesmo que o objeto esteja em um estado inválido.

O tipo de dados dos elementos vetoriais, T, deve atender aos seguintes requisitos. Caso contrário, o concurrent_vector comportamento da classe é indefinido.

  • O destruidor não deve atirar.

  • Se o construtor default ou copy lançar, o destruidor não deve ser declarado usando a virtual palavra-chave e deve funcionar corretamente com memória inicializada zero.

[Topo]

concurrent_queue Classe

A classe concurrency::concurrent_queue , assim como a classe std::queue , permite que você acesse seus elementos frontal e traseiro. A concurrent_queue classe permite operações de enqueue e dequeue seguras para simultaneidade. Aqui, simultaneidade segura significa que ponteiros ou iteradores são sempre válidos. Não é uma garantia de inicialização de elementos ou de uma ordem transversal específica. A concurrent_queue classe também fornece suporte de iterador que não é seguro para simultaneidade.

Diferenças entre concurrent_queue e queue

A concurrent_queue classe se assemelha muito à queue classe. Os pontos a seguir ilustram onde concurrent_queue difere de queue:

  • As operações de enfileiramento e desenfileiramento num objeto concurrent_queue são seguras para operações simultâneas.

  • A concurrent_queue classe fornece suporte a iteradores que não são seguros para concorrência.

  • A concurrent_queue classe não fornece os front métodos ou pop . A concurrent_queue classe substitui esses métodos definindo o método try_pop .

  • A concurrent_queue classe não fornece o back método. Portanto, você não pode fazer referência ao final da fila.

  • A concurrent_queue classe fornece o método unsafe_size em vez do size método. O unsafe_size método não é seguro para simultaneidade.

Concurrency-Safe Operações

Todos os métodos que enfileiram ou retiram a fila de um concurrent_queue objeto são seguros para simultaneidade. Aqui, simultaneidade segura significa que ponteiros ou iteradores são sempre válidos. Não é uma garantia de inicialização de elementos ou de uma ordem transversal específica.

A tabela a seguir mostra os métodos e operadores comuns concurrent_queue que são seguros para simultaneidade.

Embora o empty método seja seguro para simultaneidade, uma operação simultânea pode fazer com que a fila cresça ou diminua antes que o empty método retorne.

A tabela a seguir mostra os métodos e operadores comuns que não são seguros para simultaneidade.

Suporte ao iterador

O concurrent_queue fornece iteradores que não são seguros para simultaneidade. Recomendamos a utilização destes iteradores apenas para depuração.

Um concurrent_queue iterador percorre elementos apenas na direção avançada. A tabela a seguir mostra os operadores suportados por cada iterador.

Operador Descrição
operator++ Avança para o próximo item na fila. Este operador está sobrecarregado para fornecer as semânticas de pré-incremento e pós-incremento.
operator* Recupera uma referência ao item atual.
operator-> Recupera um ponteiro para o item atual.

[Topo]

concurrent_unordered_map Classe

A classe concurrency::concurrent_unordered_map é uma classe de container associativa que, assim como a classe std::unordered_map, controla uma sequência de comprimento variável de elementos do tipo std::pair<const Key, Ty>. Pense em um mapa não ordenado como um dicionário ao qual você pode adicionar um par de chave e valor ou procurar um valor por chave. Essa classe é útil quando você tem vários threads ou tarefas que precisam acessar simultaneamente um contêiner compartilhado, inseri-lo ou atualizá-lo.

O exemplo a seguir mostra a estrutura básica para o uso do concurrent_unordered_map. Este exemplo insere chaves de caracteres no intervalo ['a', 'i']. Como a ordem das operações é indeterminada, o valor final de cada chave também é indeterminado. No entanto, é seguro realizar as inserções em 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 obter um exemplo que usa concurrent_unordered_map para executar um mapa e reduzir a operação em paralelo, consulte Como: Executar mapa e reduzir operações em paralelo.

Diferenças entre concurrent_unordered_map e unordered_map

A concurrent_unordered_map classe se assemelha muito à unordered_map classe. Os pontos a seguir ilustram onde concurrent_unordered_map difere de unordered_map:

  • Os erasemétodos , bucket, bucket_count, e bucket_size são denominados unsafe_erase, unsafe_bucket, unsafe_bucket_count, e unsafe_bucket_size, respectivamente. A unsafe_ convenção de nomenclatura indica que esses métodos não são seguros para simultaneidade. Para obter mais informações sobre segurança de concorrência, veja Concurrency-Safe Operações.

  • As operações de inserção não invalidam ponteiros ou iteradores existentes, nem alteram a ordem dos itens que já existem no mapa. As operações de inserção e travessia podem ocorrer simultaneamente.

  • concurrent_unordered_map suporta apenas iteração sequencial.

  • A inserção não invalida nem atualiza os iteradores retornados pelo equal_range. Itens desiguais podem ser acrescentados ao final do intervalo através da inserção. O iterador de início aponta para um item igual.

Para ajudar a evitar o impasse, nenhum método de concurrent_unordered_map mantém um bloqueio quando chama o alocador de memória, as funções de hash ou qualquer outro código definido pelo utilizador. Além disso, você deve garantir que a função hash sempre avalie chaves iguais para o mesmo valor. As melhores funções de hash distribuem chaves uniformemente pelo espaço de código hash.

Concurrency-Safe Operações

A concurrent_unordered_map classe permite operações de inserção e acesso a elementos seguras para simultaneidade. As operações de inserção não invalidam ponteiros ou iteradores existentes. O acesso ao iterador e as operações de travessia também são seguros para simultaneidade. Aqui, simultaneidade segura significa que ponteiros ou iteradores são sempre válidos. Não é uma garantia de inicialização de elementos ou de uma ordem transversal específica. A tabela a seguir mostra os métodos e operadores comumente usados concurrent_unordered_map que são seguros para simultaneidade.

Embora o count método possa ser chamado com segurança a partir de threads em execução simultânea, threads diferentes podem receber resultados diferentes se um novo valor for inserido simultaneamente no contêiner.

A tabela a seguir mostra os métodos e operadores comumente usados que não são seguros para simultaneidade.

Além desses métodos, qualquer método que comece com unsafe_ também não é seguro para simultaneidade.

[Topo]

concurrent_unordered_multimap Classe

A classe concurrency::concurrent_unordered_multimap é muito semelhante à concurrent_unordered_map classe, exceto que permite que vários valores sejam mapeados para a mesma chave. Também difere de concurrent_unordered_map nos seguintes aspetos:

  • O método concurrent_unordered_multimap::insert retorna um iterador em vez de std::pair<iterator, bool>.

  • A concurrent_unordered_multimap classe não fornece operator[] nem o at método.

O exemplo a seguir mostra a estrutura básica para o uso do concurrent_unordered_multimap. Este exemplo insere chaves de caracteres no intervalo ['a', 'i']. concurrent_unordered_multimap permite que uma chave tenha vários 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]
*/

[Topo]

concurrent_unordered_set Classe

A classe concurrency::concurrent_unordered_set se assemelha muito à concurrent_unordered_map classe, exceto que ela gerencia valores em vez de pares de chave e valor. A concurrent_unordered_set classe não fornece operator[] nem o at método.

O exemplo a seguir mostra a estrutura básica para o uso do concurrent_unordered_set. Este exemplo insere valores de caracteres no intervalo ['a', 'i']. É seguro realizar as inserções em 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]
*/

[Topo]

concurrent_unordered_multiset Classe

A classe concurrency::concurrent_unordered_multiset se assemelha muito à concurrent_unordered_set classe, exceto que ela permite valores duplicados. Também difere de concurrent_unordered_set nos seguintes aspetos:

  • O método concurrent_unordered_multiset::insert retorna um iterador em vez de std::pair<iterator, bool>.

  • A concurrent_unordered_multiset classe não fornece operator[] nem o at método.

O exemplo a seguir mostra a estrutura básica para o uso do concurrent_unordered_multiset. Este exemplo insere valores de caracteres no intervalo ['a', 'i']. concurrent_unordered_multiset permite que um valor ocorra várias vezes.

// 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]
*/

[Topo]

Classe combinável

A classe concurrency::combinable fornece armazenamento local de thread reutilizável que permite executar cálculos refinados e, em seguida, mesclar esses cálculos em um resultado final. Você pode pensar em um combinable objeto como uma variável de redução.

A combinable classe é útil quando você tem um recurso que é compartilhado entre vários threads ou tarefas. A combinable classe ajuda você a eliminar o estado compartilhado, fornecendo acesso a recursos compartilhados de forma sem bloqueio. Portanto, essa classe fornece uma alternativa ao uso de um mecanismo de sincronização, por exemplo, um mutex, para sincronizar o acesso a dados compartilhados de vários threads.

Métodos e Características

A tabela a seguir mostra alguns dos métodos importantes da combinable classe. Para obter mais informações sobre todos os métodos de combinable classe, consulte Classe combinável.

Método Descrição
local Recupera uma referência à variável local associada ao contexto de thread atual.
claro Remove todas as variáveis thread-local do objeto combinable.
combinar

combine_each
Usa a função combine fornecida para gerar um valor final a partir do conjunto de todos os cálculos de thread-local.

A combinable classe é uma classe de modelo que é parametrizada no resultado final mesclado. Se você chamar o construtor padrão, o T tipo de parâmetro template deve ter um construtor padrão e um construtor copy. Se o T tipo de parâmetro template não tiver um construtor padrão, chame a versão sobrecarregada do construtor que usa uma função de inicialização como parâmetro.

Você pode armazenar dados adicionais em um combinable objeto depois de chamar os métodos combine ou combine_each . Você também pode chamar os combine métodos e combine_each várias vezes. Se nenhum valor local em um combinable objeto for alterado, os combine métodos e combine_each produzirão o mesmo resultado toda vez que forem chamados.

Exemplos

Para obter exemplos sobre como usar a combinable classe, consulte os seguintes tópicos:

[Topo]

Como: Usar contêineres paralelos para aumentar a eficiência
Mostra como usar contêineres paralelos para armazenar e acessar dados em paralelo de forma eficiente.

Como: Usar combináveis para melhorar o desempenho
Mostra como usar a classe combinable para eliminar o estado compartilhado e, assim, melhorar o desempenho.

Como: Usar combináveis para combinar conjuntos
Mostra como usar uma combine função para combinar conjuntos de dados locais de thread.

Biblioteca de Padrões Paralelos (PPL)
Descreve o PPL, que fornece um modelo de programação imperativo que promove escalabilidade e facilidade de uso para o desenvolvimento de aplicativos simultâneos.

Referência

concurrent_vetor Classe

concurrent_queue Classe

Classe concurrent_unordered_map

Classe concurrent_unordered_multimap

Classe concurrent_unordered_set

Classe concurrent_unordered_multiset

Classe combinável