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(3)
std::vector(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() и завершению программы.
Если это еще не очевидно, скажем прямо:
Итак, мы выявили все возможные точки возбуждения исключений и поняли, к каким печальным последствиям это приведёт. Что с этим можно сделать? Начнем с вопроса об исключениях, возбуждаемых в созданных нами потоках.