Condividi tramite


Contenitori e oggetti paralleli

La libreria PPL (Parallel Patterns Library) include diversi contenitori e oggetti che forniscono l'accesso thread-safe ai relativi elementi.

Un contenitore simultaneo fornisce l'accesso sicuro alla concorrenza per le operazioni più importanti. In questo caso, i puntatori o gli iteratori sicuri per la concorrenza sono sempre validi. Non è una garanzia di inizializzazione degli elementi o di un ordine attraversamento specifico. La funzionalità di questi contenitori è simile a quella fornita dalla libreria standard C++. Ad esempio, la classe concurrency::concurrent_vector è simile alla classe std::vector , ad eccezione del fatto che la concurrent_vector classe consente di aggiungere elementi in parallelo. Usare contenitori simultanei quando si dispone di codice parallelo che richiede l'accesso in lettura e scrittura allo stesso contenitore.

Un oggetto simultaneo viene condiviso simultaneamente tra i componenti. Un processo che calcola lo stato di un oggetto simultaneo in parallelo produce lo stesso risultato di un altro processo che calcola lo stesso stato in modo seriale. La classe concurrency::combinable è un esempio di un tipo di oggetto simultaneo. La combinable classe consente di eseguire calcoli in parallelo e quindi combinarli in un risultato finale. Usare oggetti simultanei quando in caso contrario si usa un meccanismo di sincronizzazione, ad esempio un mutex, per sincronizzare l'accesso a una variabile o a una risorsa condivisa.

Sezioni

In questo argomento vengono descritti in dettaglio i contenitori e gli oggetti paralleli seguenti.

Contenitori simultanei:

Oggetti simultanei:

Classe concurrent_vector

La classe concurrency::concurrent_vector è una classe contenitore di sequenza che, proprio come la classe std::vector , consente di accedere in modo casuale ai relativi elementi. La concurrent_vector classe abilita le operazioni di accodamento e accesso agli elementi indipendenti dalla concorrenza. Le operazioni di accodamento non invalidano puntatori o iteratori esistenti. Anche le operazioni di accesso e attraversamento dell'iteratore sono sicure per la concorrenza. In questo caso, i puntatori o gli iteratori sicuri per la concorrenza sono sempre validi. Non è una garanzia di inizializzazione degli elementi o di un ordine attraversamento specifico.

Differenze tra concurrent_vector e vettore

La concurrent_vector classe è simile alla vector classe . La complessità delle operazioni di accodamento, accesso agli elementi e accesso all'iteratore in un concurrent_vector oggetto è identica a quella di un vector oggetto . I punti seguenti illustrano dove concurrent_vector differisce da vector:

  • Accodamento, accesso agli elementi, accesso iteratore e operazioni di attraversamento dell'iteratore in un concurrent_vector oggetto sono indipendenti dalla concorrenza.

  • È possibile aggiungere elementi solo alla fine di un concurrent_vector oggetto . La concurrent_vector classe non fornisce il insert metodo .

  • Un concurrent_vector oggetto non usa la semantica di spostamento quando viene accodato.

  • La concurrent_vector classe non fornisce i erase metodi o pop_back . Come con vector, usare il metodo clear per rimuovere tutti gli elementi da un concurrent_vector oggetto .

  • La concurrent_vector classe non archivia i relativi elementi in modo contiguo in memoria. Pertanto, non è possibile usare la concurrent_vector classe in tutti i modi in cui è possibile usare una matrice. Ad esempio, per una variabile denominata v di tipo concurrent_vector, l'espressione &v[0]+2 produce un comportamento non definito.

  • La concurrent_vector classe definisce i metodi grow_by e grow_to_at_least . Questi metodi sono simili al metodo resize , ad eccezione del fatto che sono indipendenti dalla concorrenza.

  • Un concurrent_vector oggetto non riloca i relativi elementi quando viene accodato o ridimensionato. In questo modo, i puntatori e gli iteratori esistenti rimangono validi durante le operazioni simultanee.

  • Il runtime non definisce una versione specializzata di concurrent_vector per il tipo bool.

Operazioni sicure per la concorrenza

Tutti i metodi che aggiungono o aumentano le dimensioni di un concurrent_vector oggetto o accedono a un elemento in un concurrent_vector oggetto sono indipendenti dalla concorrenza. In questo caso, i puntatori o gli iteratori sicuri per la concorrenza sono sempre validi. Non è una garanzia di inizializzazione degli elementi o di un ordine attraversamento specifico. L'eccezione a questa regola è il resize metodo .

La tabella seguente illustra i metodi e gli operatori comuni concurrent_vector che sono sicuri per la concorrenza.

Le operazioni fornite dal runtime per la compatibilità con la libreria standard C++, ad esempio , reservenon sono sicure per la concorrenza. La tabella seguente illustra i metodi e gli operatori comuni che non sono sicuri per la concorrenza.

Le operazioni che modificano il valore degli elementi esistenti non sono sicure per la concorrenza. Utilizzare un oggetto di sincronizzazione, ad esempio un oggetto reader_writer_lock per sincronizzare le operazioni di lettura e scrittura simultanee con lo stesso elemento dati. Per altre informazioni sugli oggetti di sincronizzazione, vedere Synchronization Data Structures.

Quando si converte il codice esistente che usa vector per usare concurrent_vector, le operazioni simultanee possono causare la modifica del comportamento dell'applicazione. Si consideri ad esempio il programma seguente che esegue simultaneamente due attività su un concurrent_vector oggetto . La prima attività aggiunge elementi aggiuntivi a un concurrent_vector oggetto . La seconda attività calcola la somma di tutti gli elementi nello stesso oggetto.

// 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;
      }
   );
}

Anche se il end metodo è indipendente dalla concorrenza, una chiamata simultanea al metodo push_back fa sì che il valore restituito da venga end modificato. Il numero di elementi attraversati dall'iteratore è indeterminato. Pertanto, questo programma può produrre un risultato diverso ogni volta che viene eseguito. Quando il tipo di elemento non è semplice, è possibile che una race condition esista tra push_back le chiamate e end . Il end metodo può restituire un elemento allocato, ma non completamente inizializzato.

Sicurezza delle eccezioni

Se un'operazione di crescita o assegnazione genera un'eccezione, lo stato dell'oggetto concurrent_vector diventa non valido. Il comportamento di un concurrent_vector oggetto in uno stato non valido non è definito se non diversamente specificato. Tuttavia, il distruttore libera sempre la memoria allocata dall'oggetto, anche se l'oggetto è in uno stato non valido.

Il tipo di dati degli elementi vettoriali, T, deve soddisfare i requisiti seguenti. In caso contrario, il comportamento della concurrent_vector classe non è definito.

  • Il distruttore non deve generare.

  • Se il costruttore predefinito o di copia genera un'eccezione, il distruttore non deve essere dichiarato usando la virtual parola chiave e deve funzionare correttamente con memoria inizializzata zero.

[Torna all'inizio]

Classe concurrent_queue

La classe concurrency::concurrent_queue , proprio come la classe std::queue , consente di accedere ai relativi elementi anteriori e indietro. La concurrent_queue classe abilita le operazioni di accodamento e rimozione dalla coda sicure della concorrenza. In questo caso, i puntatori o gli iteratori sicuri per la concorrenza sono sempre validi. Non è una garanzia di inizializzazione degli elementi o di un ordine attraversamento specifico. La concurrent_queue classe fornisce anche il supporto dell'iteratore che non è sicuro per la concorrenza.

Differenze tra concurrent_queue e coda

La concurrent_queue classe è simile alla queue classe . I punti seguenti illustrano dove concurrent_queue differisce da queue:

  • Le operazioni di accodamento e rimozione dalla coda in un concurrent_queue oggetto sono sicure per la concorrenza.

  • La concurrent_queue classe fornisce il supporto dell'iteratore che non è indipendente dalla concorrenza.

  • La concurrent_queue classe non fornisce i front metodi o pop . La concurrent_queue classe sostituisce questi metodi definendo il metodo try_pop .

  • La concurrent_queue classe non fornisce il back metodo . Pertanto, non è possibile fare riferimento alla fine della coda.

  • La concurrent_queue classe fornisce il metodo unsafe_size anziché il size metodo . Il unsafe_size metodo non è indipendente dalla concorrenza.

Operazioni sicure per la concorrenza

Tutti i metodi che accodano o dequeano da un concurrent_queue oggetto sono indipendenti dalla concorrenza. In questo caso, i puntatori o gli iteratori sicuri per la concorrenza sono sempre validi. Non è una garanzia di inizializzazione degli elementi o di un ordine attraversamento specifico.

La tabella seguente illustra i metodi e gli operatori comuni concurrent_queue che sono sicuri per la concorrenza.

Anche se il empty metodo è indipendente dalla concorrenza, un'operazione simultanea può causare l'aumento o la compattazione della coda prima che il empty metodo restituisca.

La tabella seguente illustra i metodi e gli operatori comuni che non sono sicuri per la concorrenza.

Supporto iteratore

fornisce concurrent_queue iteratori che non sono sicuri per la concorrenza. È consigliabile usare questi iteratori solo per il debug.

Un concurrent_queue iteratore attraversa solo gli elementi nella direzione in avanti. La tabella seguente illustra gli operatori supportati da ogni iteratore.

Operatore Descrizione
operator++ Passa all'elemento successivo nella coda. Questo operatore è sottoposto a overload per fornire la semantica di pre-incremento e post-incremento.
operator* Recupera un riferimento all'elemento corrente.
operator-> Recupera un puntatore all'elemento corrente.

[Torna all'inizio]

Classe concurrent_unordered_map

La classe concurrency::concurrent_unordered_map è una classe contenitore associativa che, analogamente alla classe std::unordered_map , controlla una sequenza di elementi di lunghezza variabile di tipo std::p air<const Key, Ty>. Si pensi a una mappa non ordinata come dizionario che è possibile aggiungere una coppia chiave e valore o cercare un valore per chiave. Questa classe è utile quando sono presenti più thread o attività che devono accedere simultaneamente a un contenitore condiviso, inserirlo o aggiornarlo.

Nell'esempio seguente viene illustrata la struttura di base per l'uso di concurrent_unordered_map. In questo esempio vengono inseriti i tasti carattere nell'intervallo ['a', 'i']. Poiché l'ordine delle operazioni non è deterministico, anche il valore finale per ogni chiave è indeterminato. Tuttavia, è sicuro eseguire gli inserimenti in parallelo.

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

Per un esempio che usa concurrent_unordered_map per eseguire un'operazione di mapping e riduzione in parallelo, vedere Procedura: Eseguire operazioni di mapping e riduzione in parallelo.

Differenze tra concurrent_unordered_map e unordered_map

La concurrent_unordered_map classe è simile alla unordered_map classe . I punti seguenti illustrano dove concurrent_unordered_map differisce da unordered_map:

  • I erasemetodi , bucket, bucket_counte bucket_size sono denominati unsafe_eraserispettivamente , unsafe_bucket_countunsafe_bucket, e unsafe_bucket_size. La unsafe_ convenzione di denominazione indica che questi metodi non sono sicuri per la concorrenza. Per altre informazioni sulla sicurezza della concorrenza, vedere Operazioni sicure per la concorrenza.

  • Le operazioni di inserimento non invalidano puntatori o iteratori esistenti, né modificano l'ordine degli elementi già presenti nella mappa. Le operazioni di inserimento e attraversamento possono verificarsi simultaneamente.

  • concurrent_unordered_map supporta solo l'iterazione in avanti.

  • L'inserimento non invalida o aggiorna gli iteratori restituiti da equal_range. L'inserimento può aggiungere elementi diversi alla fine dell'intervallo. L'iteratore di inizio punta a un elemento uguale.

Per evitare deadlock, nessun metodo di contiene un blocco quando chiama l'allocatore di concurrent_unordered_map memoria, le funzioni hash o altro codice definito dall'utente. Inoltre, è necessario assicurarsi che la funzione hash restituisca sempre chiavi uguali allo stesso valore. Le migliori funzioni hash distribuiscono le chiavi in modo uniforme nello spazio del codice hash.

Operazioni sicure per la concorrenza

La concurrent_unordered_map classe abilita le operazioni di inserimento ed accesso agli elementi sicure per la concorrenza. Le operazioni di inserimento non invalidano puntatori o iteratori esistenti. Anche le operazioni di accesso e attraversamento dell'iteratore sono sicure per la concorrenza. In questo caso, i puntatori o gli iteratori sicuri per la concorrenza sono sempre validi. Non è una garanzia di inizializzazione degli elementi o di un ordine attraversamento specifico. La tabella seguente illustra i metodi e gli operatori comunemente usati concurrent_unordered_map che sono sicuri per la concorrenza.

Anche se il count metodo può essere chiamato in modo sicuro da thread in esecuzione simultanea, i thread diversi possono ricevere risultati diversi se un nuovo valore viene inserito contemporaneamente nel contenitore.

Nella tabella seguente vengono illustrati i metodi e gli operatori comunemente usati che non sono sicuri per la concorrenza.

Oltre a questi metodi, qualsiasi metodo che inizia con unsafe_ non è sicuro per la concorrenza.

[Torna all'inizio]

Classe concurrent_unordered_multimap

La classe concurrency::concurrent_unordered_multimap è molto simile alla concurrent_unordered_map classe , ad eccezione del fatto che consente il mapping di più valori alla stessa chiave. Differisce anche da concurrent_unordered_map nei modi seguenti:

Nell'esempio seguente viene illustrata la struttura di base per l'uso di concurrent_unordered_multimap. In questo esempio vengono inseriti i tasti carattere nell'intervallo ['a', 'i']. concurrent_unordered_multimap consente a una chiave di avere più valori.

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

[Torna all'inizio]

Classe concurrent_unordered_set

La classe concurrency::concurrent_unordered_set è molto simile alla concurrent_unordered_map classe, ad eccezione del fatto che gestisce i valori anziché coppie chiave e valore. La concurrent_unordered_set classe non fornisce operator[] né il at metodo .

Nell'esempio seguente viene illustrata la struttura di base per l'uso di concurrent_unordered_set. In questo esempio vengono inseriti valori di carattere nell'intervallo ['a', 'i']. È sicuro eseguire gli inserimenti in parallelo.

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

[Torna all'inizio]

Classe concurrent_unordered_multiset

La classe concurrency::concurrent_unordered_multiset è molto simile alla classe, ad eccezione del concurrent_unordered_set fatto che consente valori duplicati. Differisce anche da concurrent_unordered_set nei modi seguenti:

Nell'esempio seguente viene illustrata la struttura di base per l'uso di concurrent_unordered_multiset. In questo esempio vengono inseriti valori di carattere nell'intervallo ['a', 'i']. concurrent_unordered_multiset consente a un valore di verificarsi più volte.

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

[Torna all'inizio]

Classe combinable

La classe concurrency::combinable fornisce risorse di archiviazione riutilizzabili e locali del thread che consentono di eseguire calcoli con granularità fine e quindi unire tali calcoli in un risultato finale. È possibile considerare un oggetto combinable come una variabile di riduzione.

La combinable classe è utile quando si dispone di una risorsa condivisa tra più thread o attività. La combinable classe consente di eliminare lo stato condiviso fornendo l'accesso alle risorse condivise in modo senza blocchi. Pertanto, questa classe offre un'alternativa all'uso di un meccanismo di sincronizzazione, ad esempio un mutex, per sincronizzare l'accesso ai dati condivisi da più thread.

Metodi e funzionalità

La tabella seguente illustra alcuni dei metodi importanti della combinable classe . Per altre informazioni su tutti i metodi di combinable classe, vedere Classe combinabile.

metodo Descrizione
local Recupera un riferimento alla variabile locale associata al contesto del thread corrente.
deselezionare Rimuove tutte le variabili locali del thread dall'oggetto combinable .
combine

combine_each
Usa la funzione di combinazione fornita per generare un valore finale dal set di tutti i calcoli locali del thread.

La combinable classe è una classe modello con parametri per il risultato finale unito. Se si chiama il costruttore predefinito, il T tipo di parametro del modello deve avere un costruttore predefinito e un costruttore di copia. Se il T tipo di parametro del modello non dispone di un costruttore predefinito, chiamare la versione di overload del costruttore che accetta una funzione di inizializzazione come parametro.

È possibile archiviare dati aggiuntivi in un combinable oggetto dopo aver chiamato i metodi combine o combine_each . È anche possibile chiamare i combine metodi e combine_each più volte. Se non viene modificato alcun valore locale in un combinable oggetto, i combine metodi e combine_each producono lo stesso risultato ogni volta che vengono chiamati.

Esempi

Per esempi su come usare la combinable classe , vedere gli argomenti seguenti:

[Torna all'inizio]

Procedura: Usare i contenitori paralleli per aumentare l'efficienza
Illustra come usare contenitori paralleli per archiviare e accedere in modo efficiente ai dati in parallelo.

Procedura: Usare la classe combinable per migliorare le prestazioni
Illustra come usare la combinable classe per eliminare lo stato condiviso e quindi migliorare le prestazioni.

Procedura: Usare l'oggetto combinable per combinare set
Illustra come usare una combine funzione per unire set di dati locali di thread.

PPL (Parallel Patterns Library)
Descrive il PPL, che fornisce un modello di programmazione imperativo che promuove scalabilità e facilità d'uso per lo sviluppo di applicazioni simultanee.

Riferimento

Classe concurrent_vector

Classe concurrent_queue

Classe concurrent_unordered_map

Classe concurrent_unordered_multimap

Classe concurrent_unordered_set

Classe concurrent_unordered_multiset

Classe combinable