Параллельное программирование на С++ в действии. Практика разработки многопоточных программ | страница 35
Число запускаемых потоков равно минимуму из только что вычисленного максимума и количества аппаратных потоков (3): мы не хотим запускать больше потоков, чем может поддержать оборудование (это называется превышением лимита), так как из-за контекстных переключений при большем количестве потоков производительность снизится. Если функция >std::thread::hardware_concurrency()
вернула 0, то мы берем произвольно выбранное число, я решил остановиться на 2. Мы не хотим запускать слишком много потоков, потому что на одноядерной машине это только замедлило бы программу. Но и слишком мало потоков тоже плохо, так как это означало бы отказ от возможного параллелизма.
Каждый поток будет обрабатывать количество элементов, равное длине диапазона, поделенной на число потоков (4). Пусть вас не пугает случай, когда одно число нацело не делится на другое, — ниже мы рассмотрим его.
Теперь, зная, сколько необходимо потоков, мы можем создать вектор >std::vector
для хранения промежуточных результатов и вектор >std::vector
для хранения потоков (5). Отметим, что запускать нужно на один поток меньше, чем >num_threads
, потому что один поток у нас уже есть.
Запуск потоков производится в обычном цикле: мы сдвигаем итератор >block_end
в конец текущего блока (6) и запускаем новый поток для аккумулирования результатов по этому блоку (7). Начало нового блока совпадает с концом текущего (8).
После того как все потоки запущены, главный поток может обработать последний блок (9). Именно здесь обрабатывается случай деления с остатком: мы знаем, что конец последнего блока — >last
, а сколько в нем элементов, не имеет значения.
Аккумулировав результаты но последнему блоку, мы можем дождаться завершения всех запущенных потоков с помощью алгоритма >std::for_each
(10), а затем сложить частичные результаты, обратившись к >std::accumulate
(11).
Прежде чем расстаться с этим примером, полезно отметить, что в случае, когда оператор сложения, определенный в типе >T
, не ассоциативен (например, если >T
— это >float
или >double
), результаты, возвращаемые алгоритмами >parallel_accumulate
и >std::accumulate
, могут различаться из-за разбиения диапазона на блоки. Кроме того, к итераторам предъявляются более жесткие требования: они должны быть по меньшей мере однонаправленными, тогда как алгоритм >std::accumulate
может работать и с однопроходными итераторами ввода. Наконец, тип >T
должен допускать конструирование по умолчанию (удовлетворять требованиям концепции