Поделиться через


Параллельные контейнеры и объекты

Библиотека параллельных шаблонов (PPL) включает несколько контейнеров и объектов, которые обеспечивают потокобезопасный доступ к их элементам.

Одновременный контейнер предоставляет безопасный для параллелизма доступ к наиболее важным операциям. Здесь "безопасность для параллелизма" означает, что указатели или итераторы всегда допустимы. Это не гарантия инициализации элементов или определенного порядка обхода. Функциональные возможности этих контейнеров похожи на те, которые предоставляются стандартной библиотекой C++ . Например, класс параллелизма::concurrent_vector напоминает класс std::vector , за исключением того, что concurrent_vector класс позволяет добавлять элементы параллельно. Используйте параллельные контейнеры, если у вас есть параллельный код, требующий доступа для чтения и записи к одному контейнеру.

Одновременный объект совместно используется между компонентами. Процесс, вычисляющий состояние параллельного объекта, создает тот же результат, что и другой процесс, который вычисляет то же состояние последовательно. Класс concurrency::combinable является одним из примеров параллельного типа объекта. Класс combinable позволяет выполнять вычисления параллельно, а затем объединять эти вычисления в окончательный результат. Используйте параллельные объекты, если в противном случае используется механизм синхронизации, например мьютекс, для синхронизации доступа к общей переменной или ресурсу.

Разделы

В этом разделе подробно описаны следующие параллельные контейнеры и объекты.

Одновременные контейнеры:

Параллельные объекты:

Класс concurrent_vector

Класс concurrency::concurrent_vector — это класс контейнера последовательности, который, как и std::vector, позволяет случайным образом получить доступ к его элементам. Класс concurrent_vector обеспечивает безопасные для параллелизма операции добавления и доступа к элементам. Операции добавления не делают недействительными существующие указатели или итераторы. Операции доступа и обхода итератора также безопасны для параллелизма. Здесь "безопасность для параллелизма" означает, что указатели или итераторы всегда допустимы. Это не гарантия инициализации элементов или определенного порядка обхода.

Различия между concurrent_vector и вектором

Класс concurrent_vector очень похож на vector класс. Сложность операций доступа к добавлению, доступу к элементам и итератору concurrent_vector объекта совпадает с сложностью операций доступа к объекту vector . Ниже показано, где concurrent_vector отличается от vector:

  • Добавление, доступ к элементам, доступ к итераторам и их обход, выполняемые в объекте concurrent_vector безопасны для многопоточности.

  • Элементы можно добавлять только в конец concurrent_vector объекта. Класс concurrent_vector не предоставляет insert метод.

  • Объект concurrent_vector не использует семантику перемещения при добавлении к нему.

  • Класс concurrent_vector не предоставляет erase методы или pop_back методы. Как и при использовании vector, используйте метод clear для удаления всех элементов из concurrent_vector объекта.

  • Класс concurrent_vector не хранит свои элементы подряд в памяти. Таким образом, вы не можете использовать класс concurrent_vector во всех тех случаях, в которых можно использовать массив. Например, для переменной с именем v типа concurrent_vectorвыражение &v[0]+2 создает неопределенное поведение.

  • Класс concurrent_vector определяет методы grow_by и grow_to_at_least . Эти методы похожи на метод изменения размера , за исключением того, что они являются безопасными для параллелизма.

  • Объект concurrent_vector не перемещает его элементы при его добавлении или изменении размера. Это позволяет существующим указателям и итераторам оставаться действительными во время параллельных операций.

  • Среда выполнения не определяет специализированную версию concurrent_vector типа bool.

Потокобезопасные операции

Все методы, добавляющие или увеличивающие размер concurrent_vector объекта, или доступ к элементу в concurrent_vector объекте, являются безопасными для параллелизма. В этом контексте "безопасные для многопоточности" означает, что указатели или итераторы всегда допустимы. Это не гарантия инициализации элементов или определенного порядка обхода. Исключением из этого правила является resize метод.

В следующей таблице показаны распространенные concurrent_vector методы и операторы, которые являются безопасными для параллелизма.

Операции, предоставляемые средой выполнения для совместимости со стандартной библиотекой C++, такие как reserve, не являются безопасными для параллелизма. В следующей таблице показаны распространенные методы и операторы, которые не являются безопасными для параллелизма.

Операции, изменяющие значение существующих элементов, не являются безопасными для параллелизма. Используйте объект синхронизации, например объект reader_writer_lock для синхронизации параллельных операций чтения и записи с тем же элементом данных. Дополнительные сведения о объектах синхронизации см. в разделе "Структуры данных синхронизации".

При преобразовании существующего кода, используемого vector для использования concurrent_vector, одновременные операции могут привести к изменению поведения приложения. Например, рассмотрим следующую программу, которая параллельно выполняет две задачи в объекте concurrent_vector . Первая задача добавляет дополнительные элементы к объекту concurrent_vector . Вторая задача вычисляет сумму всех элементов в одном объекте.

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

Несмотря на то, что метод end является безопасным для параллельного выполнения, одновременный вызов метода push_back вызывает изменение значения, возвращаемого end. Количество элементов, которые проходит итератор, является неопределенным. Таким образом, эта программа может создавать разные результаты при каждом запуске. Если тип элемента не является тривиальным, возможно возникновение состояния гонки между вызовами push_back и end. Метод end может возвращать элемент, которому выделена память, но он не полностью инициализирован.

Безопасность исключений

Если операция роста или назначения вызывает исключение, состояние concurrent_vector объекта становится недействительным. Поведение concurrent_vector объекта, который находится в недопустимом состоянии, не определено, если не указано в противном случае. Однако деструктор всегда освобождает память, выделенную объектом, даже если объект находится в недопустимом состоянии.

Тип данных элементов Tвектора должен соответствовать следующим требованиям. В противном случае поведение concurrent_vector класса не определено.

  • Деструктор не должен вызываться.

  • Если вызывается конструктор по умолчанию или копирование, деструктор не должен быть объявлен с помощью ключевого virtual слова, и он должен работать правильно с нулевой инициализируемой памятью.

[В начало]

Класс concurrent_queue

Класс concurrency::concurrent_queue, как класс std::queue, позволяет получить доступ к его первому и последнему элементам. Класс concurrent_queue обеспечивает безопасные для многозадачности операции по добавлению в очередь и удалению из неё. Здесь "безопасность для параллелизма" означает, что указатели или итераторы всегда допустимы. Это не гарантия инициализации элементов или определенного порядка обхода. Класс concurrent_queue также предоставляет поддержку итератора, которая не является безопасной для параллелизма.

Различия между concurrent_queue и стандартной очередью

Класс concurrent_queue очень похож на queue класс. Ниже показано, где concurrent_queue отличается от queue:

  • Операции добавления и удаления из очереди для объекта concurrent_queue являются безопасными при параллельном выполнении.

  • Класс concurrent_queue предоставляет поддержку итератора, которая не является безопасной для параллелизма.

  • Класс concurrent_queue не предоставляет front методы или pop методы. Класс concurrent_queue заменяет эти методы путем определения метода try_pop .

  • Класс concurrent_queue не предоставляет back метод. Поэтому вы не можете ссылаться на конец очереди.

  • Класс concurrent_queue предоставляет метод unsafe_size вместо size метода. Метод unsafe_size не является безопасным для параллелизма.

Потокобезопасные операции

Все методы, которые вложены в объект или удаляются из concurrent_queue объекта, являются безопасными для параллелизма. Здесь "безопасность для параллелизма" означает, что указатели или итераторы всегда допустимы. Это не гарантия инициализации элементов или определенного порядка обхода.

В следующей таблице показаны распространенные concurrent_queue методы и операторы, которые являются безопасными для параллелизма.

empty Хотя метод является потокобезопасным, параллельная операция может привести к увеличению или сокращению очереди перед возвратом empty метода.

В следующей таблице показаны распространенные методы и операторы, которые не являются безопасными для параллелизма.

Поддержка итератора

Компонент concurrent_queue предоставляет итераторы, которые не потокобезопасны. Мы рекомендуем использовать эти итераторы только для отладки.

Итератор concurrent_queue проходит элементы только в направлении вперед. В следующей таблице показаны операторы, поддерживаемые каждым итератором.

Оператор Описание
operator++ Переходит к следующему элементу в очереди. Этот оператор перегружен для предоставления семантики предварительного и последующего увеличения.
operator* Извлекает ссылку на текущий элемент.
operator-> Извлекает указатель на текущий элемент.

[В начало]

Класс concurrent_unordered_map

Класс concurrency::concurrent_unordered_map — это ассоциативный контейнер, который, как и std::unordered_map, управляет последовательностью элементов переменной длины типа std::pair<const Key, Ty>. Подумайте о неупорядоченной карте как словаре, в которую можно добавить пару "ключ и значение" или искать значение по ключу. Этот класс полезен при наличии нескольких потоков или задач, которые должны одновременно обращаться к общему контейнеру, вставлять в него или обновлять его.

В следующем примере показана базовая структура для использования concurrent_unordered_map. В этом примере вставляются клавиши с символами в диапазоне ['a', 'i']. Так как порядок операций не определен, окончательное значение для каждого ключа также не определено. Однако выполнение вставок параллельно безопасно.

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

Пример, который используется concurrent_unordered_map для параллельного выполнения операции сопоставления и уменьшения, см. в статье "Практическое руководство. Выполнение операций сопоставления и уменьшения числа операций параллельно".

Различия между concurrent_unordered_map и unordered_map

Класс concurrent_unordered_map очень похож на unordered_map класс. Ниже показано, где concurrent_unordered_map отличается от unordered_map:

  • Методы erase, bucket, bucket_count, и bucket_size имеют имена unsafe_erase, unsafe_bucket, unsafe_bucket_count, и unsafe_bucket_size соответственно. Соглашение unsafe_ об именовании указывает, что эти методы не являются безопасными для параллелизма. Дополнительные сведения о безопасности параллелизма см. в разделе "Безопасные операции параллелизма".

  • Операции вставки не делают недействительными существующие указатели или итераторы, а также не изменяют порядок элементов, которые уже существуют в карте. Операции вставки и обхода могут выполняться одновременно.

  • concurrent_unordered_map поддерживает только прямую итерацию.

  • Вставка не отменяет или не обновляет итераторы, возвращаемые equal_range. Вставка может добавлять неодинаковые элементы в конец диапазона. Начальный итератор указывает на элемент, равный по значению.

Чтобы избежать взаимоблокировки, ни один метод concurrent_unordered_map не удерживает блокировку при вызове распределителя памяти, хэш-функций или другого пользовательского кода. Кроме того, необходимо убедиться, что хэш-функция всегда вычисляет равные ключи с тем же значением. Лучшие хэш-функции равномерно распределяют ключи по пространству хэш-кодов.

Потокобезопасные операции

Класс concurrent_unordered_map включает операции вставки и доступа к элементам, безопасные для параллелизма. Операции вставки не отменяют существующие указатели или итераторы. Операции доступа и обхода итератора также безопасны для параллелизма. Здесь "безопасность для параллелизма" означает, что указатели или итераторы всегда допустимы. Это не гарантия инициализации элементов или определенного порядка обхода. В следующей таблице показаны часто используемые concurrent_unordered_map методы и операторы, которые являются безопасными для параллелизма.

Несмотря на то, что метод можно безопасно вызывать из одновременно выполняющихся потоков, разные потоки могут получать различные результаты, если новое значение вставляется в контейнер в тот же момент.

В следующей таблице показаны часто используемые методы и операторы, которые не являются безопасными для параллелизма.

Помимо этих методов, любой метод, начинающийся с unsafe_ , также не является безопасным для параллелизма.

[В начало]

Класс concurrent_unordered_multimap

Класс concurrency::concurrent_unordered_multimap аналогичен классу concurrent_unordered_map, однако допускает сопоставление нескольких значений с одним ключом. Это также отличается от concurrent_unordered_map следующим образом:

  • Метод concurrent_unordered_multimap::insert возвращает итератор вместо std::pair<iterator, bool>.

  • Класс concurrent_unordered_multimap не предоставляет ни operator[], ни метод at.

В следующем примере показана базовая структура для использования concurrent_unordered_multimap. В этом примере вставляются клавиши с символами в диапазоне ['a', 'i']. concurrent_unordered_multimap позволяет ключу иметь несколько значений.

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

[В начало]

Класс concurrent_unordered_set

Класс concurrency::concurrent_unordered_set очень похож на concurrent_unordered_map класс, за исключением того, что он управляет значениями вместо пар "ключ и значение". Класс concurrent_unordered_set не предоставляет ни operator[], ни метода at.

В следующем примере показана базовая структура для использования concurrent_unordered_set. В этом примере вставляется символьное значение в диапазоне ['a', 'i']. Выполнение вставок в параллельном режиме безопасно.

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

[В начало]

Класс concurrent_unordered_multiset

Класс concurrency::concurrent_unordered_multiset очень похож на класс concurrent_unordered_set, за исключением того, что он позволяет дубликаты значений. Он также отличается от concurrent_unordered_set следующими способами:

  • Метод concurrent_unordered_multiset::insert возвращает итератор вместо std::pair<iterator, bool>.

  • Класс concurrent_unordered_multiset не предоставляет operator[] и метод at.

В следующем примере показана базовая структура для использования concurrent_unordered_multiset. В этом примере вставляются символьные значения в диапазоне ['a', 'i']. concurrent_unordered_multiset позволяет значению появляться несколько раз.

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

[В начало]

Класс «combinable»

Класс concurrency::combinable обеспечивает повторно используемое, локальное для потоков хранилище, которое позволяет выполнять детализированные вычисления, а затем объединять эти вычисления в окончательный результат. Объект combinable можно рассматривать как переменную уменьшения.

Класс combinable полезен, если у вас есть ресурс, общий для нескольких потоков или задач. Класс combinable помогает устранить общее состояние, предоставив доступ к общим ресурсам без блокировки. Таким образом, этот класс предоставляет альтернативу использованию механизма синхронизации, например мьютекса, для синхронизации доступа к общим данным из нескольких потоков.

Методы и компоненты

В следующей combinable таблице показаны некоторые важные методы класса. Дополнительные сведения обо всех методах класса см. в combinable Class.

Метод Описание
локальный Извлекает ссылку на локальную переменную, связанную с текущим контекстом потока.
очистить Удаляет все локальные переменные потока из combinable объекта.
сочетать

combine_each
Использует предоставленную функцию объединения для генерации окончательного значения из всех вычислений, локальных для потоков.

Класс combinable — это класс шаблона, параметризованный в окончательном объединенном результате. Если вызвать конструктор по умолчанию T, то тип параметра шаблона должен иметь конструктор по умолчанию и конструктор копирования. T Если тип параметра шаблона не имеет конструктора по умолчанию, вызовите перегруженную версию конструктора, которая принимает функцию инициализации в качестве параметра.

После вызова методов combine или combine_each вы можете хранить дополнительные данные в объекте combinable. Вы также можете вызывать combine методы и combine_each несколько раз. Если локальное значение в объекте combinable не изменяется, combinecombine_each методы создают одинаковый результат при каждом вызове.

Примеры

Примеры использования combinable класса см. в следующих разделах:

[В начало]

Практическое руководство. Использование параллельных контейнеров для повышения эффективности
Показывает, как использовать параллельные контейнеры для эффективного хранения и доступа к данным параллельно.

Как использовать: combinable для повышения производительности
Показывает, как использовать combinable класс для устранения общего состояния и тем самым повысить производительность.

Практическое руководство. Использование класса combinable для комбинирования наборов
Показывает, как использовать combine функцию для слияния локальных наборов данных потока.

Библиотека параллельных шаблонов
Описывает PPL, которая предоставляет императивную модель программирования, которая способствует масштабируемости и простоте использования для разработки параллельных приложений.

Справочные материалы

Класс concurrent_vector

Класс concurrent_queue

Класс concurrent_unordered_map

Класс concurrent_unordered_multimap

Класс concurrent_unordered_set

Класс concurrent_unordered_multiset

Комбинируемый класс