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() и завершению программы.

Если это еще не очевидно, скажем прямо: этот код не безопасен относительно исключений.

Делаем код безопасным относительно исключений

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

Перейти на страницу:

Похожие книги