Каждый поток будет обрабатывать количество элементов, равное длине диапазона, поделенной на число потоков (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
должен допускать конструирование по умолчанию (удовлетворять требованиям концепции results
. Такого рода изменения требований довольно типичны для параллельных алгоритмов: но самой своей природе они отличаются от последовательных алгоритмов, и это приводит к определенным последствиям в части как результатов, так и требований. Более подробно параллельные алгоритмы рассматриваются в главе 8. Стоит также отметить, что из-за невозможности вернуть значение непосредственно из потока, мы должны передавать ссылку на соответствующий элемент вектора results
. Другой способ возврата значений из потоков,
В данном случае вся необходимая потоку информация передавалась в момент его запуска — в том числе и адрес, но которому необходимо сохранить результат вычисления. Так бывает не всегда; иногда требуется каким-то образом идентифицировать потоки во время работы. Конечно, можно было бы передать какой-то идентификатор, например значение i
в листинге 2.7, но если вызов функции, которой этот идентификатор нужен, находится несколькими уровнями стека глубже, и эта функция может вызываться из любого потока, то поступать так неудобно. Проектируя библиотеку С++ Thread Library, мы предвидели этот случай, поэтому снабдили каждый поток уникальным идентификатором.
2.5. Идентификация потоков
Идентификатор потока имеет тип std::thread::id
, и получить его можно двумя способами. Во-первых, идентификатор потока, связанного с объектом std::thread
, возвращает функция-член get_id()
этого объекта. Если с объектом std::thread
не связан никакой поток, то get_id()
возвращает сконструированный по умолчанию объект типа std::thread::id
, что следует интерпретировать как «не поток». Идентификатор текущего потока можно получить также, обратившись к функции std::this_thread::get_id()
, которая также определена в заголовке
.
Объекты типа std::thread::id
можно без ограничений копировать и сравнивать, в противном случае они вряд ли могли бы играть роль идентификаторов. Если два объекта типа std::thread::id равны
, то либо они представляют один и тот же поток, либо оба содержат значение «не поток». Если же два таких объекта не равны, то либо они представляют разные потоки, либо один представляет поток, а другой содержит значение «не поток».