Parallele Container und Objekte

Die Parallel Patterns Library (PPL) enthält mehrere Container und Objekte, die threadsicheren Zugriff auf ihre Elemente bieten.

Ein gleichzeitiger Container bietet gleichzeitigen sicheren Zugriff auf die wichtigsten Vorgänge. Hier sind Parallelitätssichere Zeiger oder Iteratoren immer gültig. Es ist keine Garantie für die Elementinitialisierung oder eine bestimmte Traversalreihenfolge. Die Funktionalität dieser Container ähnelt denen, die von der C++-Standardbibliothek bereitgestellt werden. Beispielsweise ähnelt die Parallelitätsklasse::concurrent_vector Klasse der std::vector-Klasse , mit der Ausnahme, dass die concurrent_vector Klasse Elemente parallel anfügen kann. Verwenden Sie gleichzeitige Container, wenn Sie über parallelen Code verfügen, der sowohl Lese- als auch Schreibzugriff auf denselben Container erfordert.

Ein gleichzeitiges Objekt wird gleichzeitig für Komponenten freigegeben. Ein Prozess, der den Zustand eines gleichzeitigen Objekts parallel berechnet, erzeugt dasselbe Ergebnis wie ein anderer Prozess, der denselben Zustand fortlaufend berechnet. Die Parallelität::kombinationsfähige Klasse ist ein Beispiel für einen gleichzeitigen Objekttyp. Mit der combinable Klasse können Sie Berechnungen parallel durchführen und diese Berechnungen dann in einem Endergebnis kombinieren. Verwenden Sie gleichzeitige Objekte, wenn Sie andernfalls einen Synchronisierungsmechanismus verwenden würden, z. B. einen Mutex, um den Zugriff auf eine freigegebene Variable oder Ressource zu synchronisieren.

Abschnitte

In diesem Thema werden die folgenden parallelen Container und Objekte ausführlich beschrieben.

Gleichzeitige Container:

Gleichzeitige Objekte:

concurrent_vector-Klasse

Die Parallelität::concurrent_vector Klasse ist eine Sequenzcontainerklasse, mit der Sie wie die Std::Vector-Klasse zufällig auf die zugehörigen Elemente zugreifen können. Die concurrent_vector Klasse ermöglicht Parallelitätssichere Anfüge- und Elementzugriffsvorgänge. Anfügevorgänge können vorhandene Zeiger oder Iteratoren nicht ungültig machen. Iteratorzugriff und Traversalvorgänge sind ebenfalls parallel. Hier sind Parallelitätssichere Zeiger oder Iteratoren immer gültig. Es ist keine Garantie für die Elementinitialisierung oder eine bestimmte Traversalreihenfolge.

Unterschiede zwischen concurrent_vector und Vektoren

Die concurrent_vector Klasse ähnelt vector der Klasse genau. Die Komplexität von Anfüge-, Elementzugriffs- und Iteratorzugriffsvorgängen für ein concurrent_vector Objekt ist identisch mit einem vector Objekt. Die folgenden Punkte veranschaulichen, wo concurrent_vector sich die Unterschiede unterscheiden vector:

  • Anfüge-, Elementzugriff, Iteratorzugriff und Iterator-Traversalvorgänge für ein concurrent_vector Objekt sind parallel.

  • Sie können Elemente nur am Ende eines concurrent_vector Objekts hinzufügen. Die concurrent_vector Klasse stellt die insert Methode nicht bereit.

  • Ein concurrent_vector Objekt verwendet beim Anfügen keine Bewegungssemantik .

  • Die concurrent_vector Klasse stellt weder die Methoden noch pop_back die erase Methoden bereit. Verwenden Sie wie bei diesem Verfahren vectordie clear-Methode , um alle Elemente aus einem concurrent_vector Objekt zu entfernen.

  • Die concurrent_vector Klasse speichert ihre Elemente nicht zusammenhängend im Speicher. Daher können Sie die concurrent_vector Klasse nicht auf alle Arten verwenden, wie Sie ein Array verwenden können. For example, for a variable named v of type concurrent_vector, the expression &v[0]+2 produces undefined behavior.

  • Die concurrent_vector Klasse definiert die methoden grow_by und grow_to_at_least . Diese Methoden ähneln der Resize-Methode , mit der Ausnahme, dass sie Parallelitätssicher sind.

  • Ein concurrent_vector Objekt verschlegt seine Elemente nicht, wenn Sie es anfügen oder dessen Größe ändern. Dadurch können vorhandene Zeiger und Iteratoren während gleichzeitiger Vorgänge wieder Standard gültig sein.

  • Die Laufzeit definiert keine spezielle Version vom concurrent_vector Typ bool.

Parallelitäts-Tresor Vorgänge

Alle Methoden, die die Größe eines concurrent_vector Objekts anfügen oder vergrößern oder auf ein Element in einem concurrent_vector Objekt zugreifen, sind Parallelitätssicher. Hier sind Parallelitätssichere Zeiger oder Iteratoren immer gültig. Es ist keine Garantie für die Elementinitialisierung oder eine bestimmte Traversalreihenfolge. Die Ausnahme von dieser Regel ist die resize Methode.

Die folgende Tabelle zeigt die gängigen concurrent_vector Methoden und Operatoren, die Parallelitätssicher sind.

Vorgänge, die von der Laufzeit zur Kompatibilität mit der C++-Standardbibliothek bereitgestellt werden, reservesind z. B. nicht parallel. In der folgenden Tabelle sind die gängigen Methoden und Operatoren aufgeführt, die nicht parallel sind.

Vorgänge, die den Wert vorhandener Elemente ändern, sind nicht parallel. Verwenden Sie ein Synchronisierungsobjekt wie ein reader_writer_lock-Objekt , um gleichzeitige Lese- und Schreibvorgänge mit demselben Datenelement zu synchronisieren. Weitere Informationen zu Synchronisierungsobjekten finden Sie unter "Synchronisierungsdatenstrukturen".

Wenn Sie vorhandenen Code konvertieren, der zur Verwendung verwendet concurrent_vectorwirdvector, können gleichzeitige Vorgänge dazu führen, dass sich das Verhalten Ihrer Anwendung ändert. Betrachten Sie beispielsweise das folgende Programm, das gleichzeitig zwei Aufgaben für ein concurrent_vector Objekt ausführt. Die erste Aufgabe fügt zusätzliche Elemente an ein concurrent_vector Objekt an. Der zweite Vorgang berechnet die Summe aller Elemente im selben Objekt.

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

Obwohl die end Methode parallel ist, führt ein gleichzeitiger Aufruf der push_back-Methode dazu, dass sich der Wert ändert, der zurückgegeben end wird. Die Anzahl der Elemente, die der Iterator durchläuft, ist unbestimmt. Daher kann dieses Programm jedes Mal ein anderes Ergebnis erzielen, wenn Sie es ausführen. Wenn der Elementtyp nicht trivial ist, ist es möglich, dass eine Racebedingung zwischen push_back und end Aufrufen vorhanden ist. Die end Methode gibt möglicherweise ein zugeordnetes Element zurück, aber nicht vollständig initialisiert.

Ausnahme Tresor ty

Wenn ein Wachstums- oder Zuordnungsvorgang eine Ausnahme auslöst, wird der Status des concurrent_vector Objekts ungültig. Das Verhalten eines concurrent_vector Objekts, das sich in einem ungültigen Zustand befindet, ist nicht definiert, sofern nicht anders angegeben. Der Destruktor gibt jedoch immer den Speicher frei, den das Objekt zuweist, auch wenn sich das Objekt in einem ungültigen Zustand befindet.

Der Datentyp der Vektorelemente Tmuss die folgenden Anforderungen erfüllen. Andernfalls ist das Verhalten der concurrent_vector Klasse nicht definiert.

  • Der Destruktor darf nicht ausgelöst werden.

  • Wenn der Standard- oder Kopierkonstruktor ausgelöst wird, darf der Destruktor nicht mithilfe der virtual Schlüsselwort (keyword) deklariert werden, und er muss ordnungsgemäß mit null initialisiertem Arbeitsspeicher funktionieren.

[Nach oben]

concurrent_queue-Klasse

Mit der Parallelität::concurrent_queue Klasse können Sie genau wie die std::queue-Klasse auf die Vorder- und Rückseitenelemente zugreifen. Die concurrent_queue Klasse ermöglicht Parallelitätssichere Enqueue- und Dequeue-Vorgänge. Hier sind Parallelitätssichere Zeiger oder Iteratoren immer gültig. Es ist keine Garantie für die Elementinitialisierung oder eine bestimmte Traversalreihenfolge. Die concurrent_queue Klasse bietet auch Iteratorunterstützung, die nicht parallel ist.

Unterschiede zwischen concurrent_queue und Warteschlange

Die concurrent_queue Klasse ähnelt queue der Klasse genau. Die folgenden Punkte veranschaulichen, wo concurrent_queue sich die Unterschiede unterscheiden queue:

  • Enqueue- und Dequeue-Vorgänge für ein concurrent_queue Objekt sind parallelsicher.

  • Die concurrent_queue Klasse bietet Iteratorunterstützung, die nicht parallel ist.

  • Die concurrent_queue Klasse stellt weder die Methoden noch pop die front Methoden bereit. Die concurrent_queue Klasse ersetzt diese Methoden durch Definieren der try_pop-Methode .

  • Die concurrent_queue Klasse stellt die back Methode nicht bereit. Daher können Sie nicht auf das Ende der Warteschlange verweisen.

  • Die concurrent_queue Klasse stellt die unsafe_size-Methode anstelle der size Methode bereit. Die unsafe_size Methode ist nicht parallel.

Parallelitäts-Tresor Vorgänge

Alle Methoden, die von einem concurrent_queue Objekt in eine Queue queue oder dequeue führen, sind parallelsicher. Hier sind Parallelitätssichere Zeiger oder Iteratoren immer gültig. Es ist keine Garantie für die Elementinitialisierung oder eine bestimmte Traversalreihenfolge.

Die folgende Tabelle zeigt die gängigen concurrent_queue Methoden und Operatoren, die Parallelitätssicher sind.

Obwohl die empty Methode parallel ist, kann ein gleichzeitiger Vorgang dazu führen, dass die Warteschlange vergrößert oder verkleinern wird, bevor die empty Methode zurückgegeben wird.

In der folgenden Tabelle sind die gängigen Methoden und Operatoren aufgeführt, die nicht parallel sind.

Iteratorunterstützung

Die concurrent_queue Iteratoren bieten keine Parallelitätssicher. Es wird empfohlen, diese Iteratoren nur für das Debuggen zu verwenden.

Ein concurrent_queue Iterator durchläuft Elemente nur in Vorwärtsrichtung. In der folgenden Tabelle sind die Operatoren aufgeführt, die von jedem Iterator unterstützt werden.

Operator Beschreibung
operator++ Wechselt zum nächsten Element in der Warteschlange. Dieser Operator wird überladen, um sowohl präinkrementierte als auch postinkrementierte Semantik bereitzustellen.
operator* Ruft einen Verweis auf das aktuelle Element ab.
operator-> Ruft einen Zeiger auf das aktuelle Element ab.

[Nach oben]

concurrent_unordered_map-Klasse

Die Parallelität::concurrent_unordered_map Klasse ist eine assoziative Containerklasse, die genau wie die Klasse std::unordered_map eine unterschiedliche Abfolge von Elementen vom Typ "std::p air<const Key" (Ty>) steuert. Stellen Sie sich eine ungeordnete Karte als Wörterbuch vor, das Sie einem Schlüssel- und Wertpaar hinzufügen oder einen Wert nach Schlüssel nachschlagen können. Diese Klasse ist nützlich, wenn Sie über mehrere Threads oder Aufgaben verfügen, die gleichzeitig auf einen freigegebenen Container zugreifen, darin einfügen oder aktualisieren müssen.

Das folgende Beispiel zeigt die grundlegende Struktur für die Verwendung concurrent_unordered_map. In diesem Beispiel werden Zeichentasten in den Bereich ['a', 'i'] eingefügt. Da die Reihenfolge der Vorgänge nicht festgelegt ist, ist auch der endgültige Wert für jeden Schlüssel unbestimmt. Es ist jedoch sicher, die Einfügungen parallel durchzuführen.

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

Ein Beispiel, das zum Ausführen einer Karte und zur Reduzierung des Vorgangs parallel verwendet concurrent_unordered_map wird, finden Sie unter How to: Perform Map and Reduce Operations in Parallel.

Unterschiede zwischen concurrent_unordered_map und unordered_map

Die concurrent_unordered_map Klasse ähnelt unordered_map der Klasse genau. Die folgenden Punkte veranschaulichen, wo concurrent_unordered_map sich die Unterschiede unterscheiden unordered_map:

  • Die erase, bucket, bucket_count, und bucket_size Methoden sind benannt unsafe_erase, unsafe_bucket, unsafe_bucket_count, und unsafe_bucket_size, bzw. . Die unsafe_ Benennungskonvention gibt an, dass diese Methoden nicht parallel sind. Weitere Informationen zur Parallelitätssicherheit finden Sie unter Concurrency-Tresor Operations.

  • Einfügevorgänge ungültig machen weder vorhandene Zeiger noch Iteratoren, noch ändern sie die Reihenfolge der Elemente, die bereits in der Karte vorhanden sind. Einfüge- und Durchlaufvorgänge können gleichzeitig ausgeführt werden.

  • concurrent_unordered_map unterstützt nur die Weiterleitungsiteration.

  • Die Einfügemarke wird nicht ungültig oder aktualisiert die Iteratoren, die von equal_range. Das Einfügen kann ungleiche Elemente an das Ende des Bereichs anfügen. Der Anfangs iterator zeigt auf ein gleichheitselement.

Um Deadlock zu vermeiden, enthält keine Methode eine concurrent_unordered_map Sperre, wenn sie den Speicherzuordnungs-, Hashfunktionen oder anderen benutzerdefinierten Code aufruft. Außerdem müssen Sie sicherstellen, dass die Hashfunktion immer gleich Schlüssel zum gleichen Wert auswertet. Die besten Hashfunktionen verteilen Schlüssel einheitlich über den Hashcodebereich.

Parallelitäts-Tresor Vorgänge

Die concurrent_unordered_map Klasse ermöglicht Parallelitätssichere Einfüge- und Elementzugriffsvorgänge. Einfügevorgänge können vorhandene Zeiger oder Iteratoren nicht ungültig machen. Iteratorzugriff und Traversalvorgänge sind ebenfalls parallel. Hier sind Parallelitätssichere Zeiger oder Iteratoren immer gültig. Es ist keine Garantie für die Elementinitialisierung oder eine bestimmte Traversalreihenfolge. In der folgenden Tabelle sind die häufig verwendeten concurrent_unordered_map Methoden und Operatoren aufgeführt, die Parallelitätssicher sind.

Obwohl die count Methode sicher von gleichzeitig ausgeführten Threads aufgerufen werden kann, können unterschiedliche Threads unterschiedliche Ergebnisse erhalten, wenn ein neuer Wert gleichzeitig in den Container eingefügt wird.

In der folgenden Tabelle sind die häufig verwendeten Methoden und Operatoren aufgeführt, die nicht parallel sind.

Zusätzlich zu diesen Methoden ist jede Methode, die beginnt unsafe_ , auch nicht parallel.

[Nach oben]

concurrent_unordered_multimap-Klasse

Die Parallelität::concurrent_unordered_multimap Klasse ähnelt der concurrent_unordered_map Klasse eng, mit der Ausnahme, dass mehrere Werte demselben Schlüssel zugeordnet werden können. Es unterscheidet sich auch von concurrent_unordered_map den folgenden Methoden:

Das folgende Beispiel zeigt die grundlegende Struktur für die Verwendung concurrent_unordered_multimap. In diesem Beispiel werden Zeichentasten in den Bereich ['a', 'i'] eingefügt. concurrent_unordered_multimap ermöglicht es einem Schlüssel, mehrere Werte zu haben.

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

[Nach oben]

concurrent_unordered_set-Klasse

Die Parallelität::concurrent_unordered_set Klasse ähnelt der concurrent_unordered_map Klasse eng, mit der Ausnahme, dass Werte anstelle von Schlüssel- und Wertpaaren verwaltet werden. Die concurrent_unordered_set Klasse stellt weder die Methode noch die at Methode bereitoperator[].

Das folgende Beispiel zeigt die grundlegende Struktur für die Verwendung concurrent_unordered_set. In diesem Beispiel werden Zeichenwerte in den Bereich ['a', 'i'] eingefügt. Es ist sicher, die Einfügungen parallel durchzuführen.

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

[Nach oben]

concurrent_unordered_multiset-Klasse

Die Parallelität::concurrent_unordered_multiset-Klasse ähnelt der concurrent_unordered_set Klasse eng, mit der Ausnahme, dass sie doppelte Werte zulässt. Es unterscheidet sich auch von concurrent_unordered_set den folgenden Methoden:

Das folgende Beispiel zeigt die grundlegende Struktur für die Verwendung concurrent_unordered_multiset. In diesem Beispiel werden Zeichenwerte in den Bereich ['a', 'i'] eingefügt. concurrent_unordered_multiset aktiviert, dass ein Wert mehrmals auftritt.

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

[Nach oben]

combinable-Klasse

Die parallele Klasse::kombinationsfähige Klasse bietet wiederverwendbaren , threadlokalen Speicher, mit dem Sie differenzierte Berechnungen durchführen und diese Berechnungen dann in ein Endergebnis zusammenführen können. Stellen Sie sich ein combinable-Objekt wie eine Reduktionsvariable vor.

Die combinable Klasse ist nützlich, wenn Sie über eine Ressource verfügen, die von mehreren Threads oder Vorgängen gemeinsam genutzt wird. Die combinable Klasse hilft Ihnen, den freigegebenen Zustand zu beseitigen, indem Sie zugriff auf freigegebene Ressourcen auf sperrfreie Weise bereitstellen. Daher bietet diese Klasse eine Alternative zur Verwendung eines Synchronisierungsmechanismus, z. B. einen Mutex, um den Zugriff auf freigegebene Daten aus mehreren Threads zu synchronisieren.

Methoden und Features

In der folgenden Tabelle sind einige der wichtigen Methoden der combinable Klasse aufgeführt. Weitere Informationen zu allen combinable Klassenmethoden finden Sie unter "Kombinierbare Klasse".

Methode Beschreibung
local Ruft einen Verweis auf die lokale Variable ab, die dem aktuellen Threadkontext zugeordnet ist.
clear Entfernt alle threadlokalen Variablen aus dem combinable Objekt.
combine

combine_each
Verwendet die bereitgestellte Kombinationsfunktion, um einen endgültigen Wert aus der Gruppe aller threadlokalen Berechnungen zu generieren.

Die combinable Klasse ist eine Vorlagenklasse, die für das endgültige zusammengeführte Ergebnis parametrisiert wird. Wenn Sie den Standardkonstruktor aufrufen, muss der T Vorlagenparametertyp über einen Standardkonstruktor und einen Kopierkonstruktor verfügen. Wenn der T Vorlagenparametertyp keinen Standardkonstruktor aufweist, rufen Sie die überladene Version des Konstruktors auf, die eine Initialisierungsfunktion als Parameter verwendet.

Sie können zusätzliche Daten in einem combinable Objekt speichern, nachdem Sie die Combine - oder combine_each-Methoden aufgerufen haben. Sie können die combine Methoden combine_each auch mehrmals aufrufen. Wenn sich kein lokaler Wert in einem combinable Objekt ändert, erzeugen die combine Methoden combine_each jedes Mal, wenn sie aufgerufen werden, dasselbe Ergebnis.

Beispiele

Beispiele zur Verwendung der combinable Klasse finden Sie in den folgenden Themen:

[Nach oben]

Vorgehensweise: Erhöhen der Effizienz mithilfe von parallelen Containern
Zeigt, wie parallele Container verwendet werden, um Daten effizient zu speichern und parallel darauf zuzugreifen.

Vorgehensweise: Verbessern der Leistung mithilfe von combinable
Zeigt, wie Sie die Klasse verwenden, um den combinable freigegebenen Zustand zu beseitigen und dadurch die Leistung zu verbessern.

Vorgehensweise: Kombinieren von Gruppen mithilfe von combinable
Zeigt, wie eine Funktion zum Zusammenführen von threadlokalen Datensätzen verwendet combine wird.

Parallel Patterns Library (PPL)
Beschreibt die PPL, die ein imperatives Programmiermodell bereitstellt, das Skalierbarkeit und Benutzerfreundlichkeit für die Entwicklung gleichzeitiger Anwendungen fördert.

Verweis

concurrent_vector-Klasse

concurrent_queue-Klasse

concurrent_unordered_map-Klasse

concurrent_unordered_multimap-Klasse

concurrent_unordered_set-Klasse

concurrent_unordered_multiset-Klasse

combinable-Klasse