Энтони Уильямс - Параллельное программирование на С++ в действии. Практика разработки многопоточных программ
- Название:Параллельное программирование на С++ в действии. Практика разработки многопоточных программ
- Автор:
- Жанр:
- Издательство:ДМК Пресс
- Год:2012
- Город:Москва
- ISBN:978-5-94074-448-1
- Рейтинг:
- Избранное:Добавить в избранное
-
Отзывы:
-
Ваша оценка:
Энтони Уильямс - Параллельное программирование на С++ в действии. Практика разработки многопоточных программ краткое содержание
Книга «Параллельное программирование на С++ в действии» не предполагает предварительных знаний в этой области. Вдумчиво читая ее, вы научитесь писать надежные и элегантные многопоточные программы на С++11. Вы узнаете о том, что такое потоковая модель памяти, и о том, какие средства поддержки многопоточности, в том числе запуска и синхронизации потоков, имеются в стандартной библиотеке. Попутно вы познакомитесь с различными нетривиальными проблемами программирования в условиях параллелизма.
Параллельное программирование на С++ в действии. Практика разработки многопоточных программ - читать онлайн бесплатно полную версию (весь текст целиком)
Интервал:
Закладка:
Один из способов проверить, приводит ли такого рода ложное разделение к проблемам, — добавить большие разделительные блоки фиктивных данных между данными, к которым одновременно обращаются разные потоки. Например, следующая структура:
struct protected_data {│
65536 на несколько
std::mutex m; │
порядков больше, чем
char padding[65536]; ←┘
длина строки кэша
my_data data_to_protect;
};
удобна для проверки конкуренции за мьютекс, а структура
struct my_data {
data_item1 d1;
data_item2 d2;
char padding[65536];
};
my_data some_array[256];
— для проверки ложного разделения данных массива. Если в результате производительность повысится, значит, ложное разделение составляет проблему, и тогда можно либо оставить заполнитель, либо устранить ложное разделение, по-другому организовав доступ к данным.
Разумеется, порядок доступа к данным — не единственное, что нужно принимать во внимание при проектировании параллельных программ. Рассмотрим некоторые другие аспекты.
8.4. Дополнительные соображения при проектировании параллельных программ
До сих пор мы в этой главе рассматривали различные способы распределения работы между потоками, факторы, влияющие на производительность, и то, как от них зависит выбор порядка доступа к данным и самой структуры данных. Но этим проблематика проектирования параллельных программ не исчерпывается. Необходимо еще принимать во внимание такие вещи, как безопасность относительно исключений и масштабируемость. Говорят, что программа масштабируется, если ее производительность (в терминах повышения быстродействия или увеличения пропускной способности) возрастает при добавлении новых процессорных ядер. В идеале производительность должна расти линейно, то есть система с 100 процессорами должна работать в 100 раз быстрее системы с одним процессором.
Даже немасштабируемая программа может быть работоспособной — в конце концов, однопоточные приложения в этом смысле, безусловно, не масштабируемы — но вот безопасность относительно исключений — вопрос, напрямую связанный с корректностью. Если программа не безопасна относительно исключений, то может наблюдаться нарушение инвариантов, состояния гонки и даже аварийное завершение. Вот этим вопросом мы сейчас и займемся.
8.4.1. Безопасность относительно исключений в параллельных алгоритмах
Безопасность относительно исключений — необходимая составная часть любой приличной программы на С++, и параллельные программы — не исключение. На самом деле, при разработке параллельных алгоритмов часто требуется уделять исключениям даже больше внимания. Если какая-то операция в последовательном алгоритме возбуждает исключение, то алгоритм должен лишь позаботиться о предотвращении утечек памяти и нарушения собственных инвариантов, а потом может передать исключение вызывающей программе для обработки. В параллельных же алгоритмах многие операции выполняются в разных потоках. В этом случае исключение невозможно распространить вверх по стеку вызовов, потому что у каждого потока свой стек. Если выход из функции потока производится в результате исключения, то приложение завершается.
В качестве конкретного примера рассмотрим еще раз функцию parallel_accumulate
из листинга 2.8, который воспроизведен ниже.
Листинг 8.2.Наивная параллельная организация std::accumulate
(из листинга 2.8)
template
struct accumulate_block {
void operator()(Iterator first, Iterator last, T& result) {
result = std::accumulate(first, last, result); ←
(1)
}
};
template
T parallel_accumulate(Iterator first, Iterator last, T init) {
unsigned long const length = std::distance(first, last);←
(2)
if (!length)
return init;
unsigned long const min_per_thread = 25;
unsigned long const max_threads =
(length + min_per_thread - 1) / min_per_thread;
unsigned long const hardware_threads =
std::thread::hardware_concurrency();
unsigned long const num_threads =
std::min(
hardware_threads != 0 ? hardware_threads : 2, max_threads);
unsigned long const block_size = length / num_threads;
std: :vector results (num_threads); ←
(3)
std::vector threads(num_threads - 1); ←
(4)
Iterator block_start = first; ←
(5)
for (unsigned long i = 0; i < (num_threads - 1); ++i) {
Iterator block_end = block_start; ←
(6)
std::advance(block_end, block_size);
threads[i] = std::thread( ←
(7)
accumulate_block(),
block_start, block_end, std::ref(results[i]));
block_start = block_end; ←
(8)
}
accumulate_block()(
block_start, last, results[num_threads - 1]);←
(9)
std::for_each(threads.begin(), threads.end(),
std::mem_fn(&std::thread::join));
return
std::accumulate(results.begin(), results.end(), init); ←
(10)
}
Посмотрим, где могут возникнуть исключения. Вообще говоря, это вызовы библиотечных функций, которые могут возбуждать исключения, а также операции, определенные в пользовательском типе.
Итак, начнем. В точке (2)мы обращаемся к функции distance
, которая выполняет операции над пользовательским типом итератора. Поскольку мы еще не начали работу, и обращение к этой функции произведено из вызывающего потока, то тут всё нормально. Далее мы выделяем память для векторов results
(3)и threads
(4). И эти обращения произведены из вызывающего потока до начала работы и до создания новых потоков, так что и здесь всё хорошо. Разумеется, если конструктор threads
возбудит исключение, то нужно будет освободить память, выделенную для results
, но об этом позаботится деструктор.
С инициализацией объекта block_start
(5)всё чисто по тем же причинам, так что перейдём к операциям в цикле запуска потоков (6), (7), (8). Если после создания первого же потока (7)возникнет исключение, и мы его не перехватим, появится проблема; деструкторы объектов std::thread
вызывают std::terminate
, что приводит к аварийному завершению программы. Нехорошо.
Обращение к accumulate_block
в точке (9)может возбуждать исключения — с точно такими же последствиями: объекты потоков будут уничтожены, а их деструкторы вызовут std::terminate
. С другой стороны, исключение, возбуждаемое в последнем обращении к std::accumulate
(10), не так опасно, потому что к этому моменту все потоки уже присоединились к вызывающему.
Таким образом, обращения к accumulate_block
из новых потоков могут возбуждать исключения в точке (1). Так как блоки catch
отсутствуют, то исключение останется необработанным и приведёт к вызову std::terminate()
и завершению программы.
Если это еще не очевидно, скажем прямо: этот код не безопасен относительно исключений .
Итак, мы выявили все возможные точки возбуждения исключений и поняли, к каким печальным последствиям это приведёт. Что с этим можно сделать? Начнем с вопроса об исключениях, возбуждаемых в созданных нами потоках.
Читать дальшеИнтервал:
Закладка: