понедельник, 14 ноября 2011 г.

Параллельный зоопарк

Рассказывая про методы многопоточного программирования в предыдущих заметках, я регулярно обращался за помощью к библиотеке Concurrency Runtime. Эта прекрасная библиотека имеет 2 недостатка: во-первых, она доступна только в составе Microsoft Visual Studio 2010 и выше; во-вторых, она работает только в ОС Windows начиная с версии XP с 3-им сервис-паком. В данной заметке я хочу рассмотреть некоторые альтернативные библиотеки, для многопоточного программирования, лишённые таких ограничений.

Intel Threading Building Blocks

Библиотека Threading Building Blocks от Intel является наиболее серьёзным конкурентом Concurency Runtime`у от Microsoft. Местами она даже превосходит последнюю по функционалу. В частности TBB содержит гораздо более богатый набор синхронизирующих примитивов, включая абстракцию для interlocked-операций (класс tbb::atomic), полностью отсутствующую в Concurrency Runtime. Другой пример полезной функциональности, имеющейся в TBB и незаслуженно обойдённой в библиотеке от Microsoft – это Thread Local Storage. В TBB он реализуется с помощью 2ух классов: tbb::combinable и tbb::enumerable_thread_specific.

На первый взгляд может показаться, что TBB содержит гораздо больше параллельных алгоритмов, чем Parallel Patterns Library (часть Concurency Runtime). Но присмотревшись внимательнее, понимаешь, что единственным уникальным алгоритмом, не имеющим аналога в PPL, является tbb::parallel_sort. Всё остальное многообразие алгоритмов (tbb::parallel_for, tbb::parallel_reduce, tbb::parallel_scan, tbb::parallel_do, tbb::parallel_for_each и tbb::parallel_invoke) функционально сводится к 3ём алгоритмам из PPL: Concurrency::parallel_for, Concurrency::parallel_for_each и Concurrency::parallel_invoke. Практически все итерирующие алгоритмы TBB позволяют с определённой степенью настраивать стратегию разделения итерируемой последовательности между потоками. Для этого введён набор классов-диапазонов (например, tbb::blocked_range) и классов-разделителей (например, tbb::auto_partinioner). Использование данной возможности усложняет код, но при этом совершенно бесполезно для большинства решаемых задач. Сравните два функционально-идентичных примера:
// iterate from 1 to 1000 and execute
// the lambda-function for each number
tbb::parallel_for(
    1, 1000,
    [](int i){
        std::cout
           
<< ::GetCurrentThreadId() << ": " << i
            << std::endl;
    });

// split range [1, 1000) into several ranges with size <= 250,
// iterate over these ranges and execute the lambda-function
// for each SUB-RANGE
tbb::parallel_for(
    tbb::blocked_range<int>(1, 1000, 250),
    [](const tbb::blocked_range<int> range){
        for (int i = range.begin(); i != range.end(); ++i) {
            std::cout
               
<< ::GetCurrentThreadId() << ": " << i
                << std::endl;
        }
    });

С агентами и Data Flow парадигмой в TBB гораздо хуже. Агентов нет совсем. Конвейеры данных под именем pipeline имеют весьма ограниченную реализацию, перегруженную низкоуровневыми деталями:
std::stringstream buffer;
buffer << "This is a test for the data-flow pattern.";

tbb::filter_t<void, char> reader(
    // this means to execute the body in serial
    // and save order of returned elements
    tbb::filter::serial_in_order,
    [&buffer](tbb::flow_control& fc) -> char
    {
        while(buffer != 0)
        {
            char ch;
            buffer.get(ch);
            if (buffer.good()) {
                return ch;
            }
        }

        fc.stop();
        return '\0';
    } );

tbb::filter_t<char, char> transformer(
    // this means to execute the body in parallel
    // for the elements which were returned on the
    // previouse step
    tbb::filter::parallel,
    [] (char ch) -> char { return std::toupper(ch); } );

tbb::filter_t<char, void> writer(
    // this means to execute the body in serial
    // for the elements which were returned on the
    // previouse step in the same order as
    // they were returned from the the 1st filter
    // before the transformation
    tbb::filter::serial_in_order,
    [] (char ch) { std::cout << ch; });

tbb::parallel_pipeline(10, reader & transformer & writer);

Библиотека доступна для следующих операционных систем: Windows, Linux и MacOS X. И даже есть надежда, что будет работать на последних версиях процессоров AMD, а не только Intel (хотя вот этот момент я бы тщательно проверил, прежде чем начинать использование). Поэтому, если в вашем приложении вы хотите использовать параллельные алгоритмы, конкурентные контейнеры, Data Flow подход, но вам нужна поддержка нескольких операционных систем, выбор в пользу TBB очевиден. Если же операционная система одна – Windows, то я бы остановился на Concurrency Runtime. В конце-концов есть шанс, что в этом случае ваше приложение побежит даже на ARM-процессорах, после выхода Windows 8.

OpenMP
OpenMP – это стандарт API, разработанный в 1997 году для написания портируемых многопоточных приложений. Изначально он предназначался для языка Fortran. Но уже год спустя (в 1998-ом) появилась реализация стандарта для С/C++. Самая последняя версия – OpenMP 3.0 – увидела свет в 2008-ом году. Самой же массовой и широко используемой версией на данный момент является 2-ая версия стандарта: OpenMP 2.0.

Стандарт представляет собой совокупность библиотечных функций, переменных окружения и директив компилятора для языков C/C++ и Fortran. В случае С++ функции подключаются с помощью заголовочного файла omp.h, а директивы компилятора выполнены как расширения директивы #pragma с ключевым словом omp.

Возможности OpenMP по сравнению с Concurrency Runtime и TBB весьма ограничены. По-сути он содержит лишь платформо-независимые средства для параллельного запуска блоков кода, включая организацию простейших параллельных циклов, синхронизации и доступа к памяти. Главный плюс OpenMP – это простота использования и прозрачность получаемого кода. Вот как будет выглядеть пример перебора чисел от 1 до 1000 с выводом на консоль, если реализовать его с помощью OpenMP (сравните с аналогичной реализацией для TBB, приведённой выше):
#pragma omp parallel for
for (int i = 1; i < 1000; ++i) {
    std::cout
        << ::GetCurrentThreadId() << ": " << i
        << std::endl;
}

Список компиляторов, поддерживающих OpenMP, весьма обширен. Он включает и Visual С++, и GCC, и Intel Compiler (причём как для C++, так и для Fortran). Аналогично обстоят дела и с платформами: Windows, Linux, Solaris, MacOS X, AIX. Более детальную информацию можно найти здесь.

Boost.Threads
Без краткого упоминания Boost.Threads данный обзор был бы не полным. Но упоминание будет действительно кратким.

В состав Boost входит небольшой набор примитивов для многопоточной разработки, позволяющий абстрагироваться от конкретной платформы. Сюда входят: класс потока – class thread; стандартная абстракция для группирования потоков (песочница), позволяющая реализовать операции группового ожидания и прерывания – class thread_group; некоторое количество разнообразных синхронизирующих примитивов и функций ожидания; Thread Local Storageclass thread_specific_ptr.

Подобный функционал есть у многих продвинутых платформо-независимых библиотек классов для C++. Более того, новый стандарт C++ от 2011 года содержит часть аналогичных возможностей в составе стандартной библиотеки, а именно: класс потока, TLS, синхронизирующие примитивы.

Таким образом, если вам необходимы всего лишь элементарные примитивы для многопоточной разработки, вроде потоков и мьютексов, и вам важна портируемость, я бы предложил воспользоваться boost`ом, тем более он содержит много полезного кроме многопоточности. Другой вариант: найти реализацию стандартной библиотеки C++, соответствующую последнему стандарту.

Комментариев нет:

Отправить комментарий