C++17 STL Стандартная библиотека шаблонов — страница 97 из 119

Однако вместо того, чтобы разрабатывать собственные сложные параллельные алгоритмы с помощью

std::thread
,
std::async
или внешних библиотек, можно распараллелить выполнение стандартных задач способом, не зависящим от операционной системы.


Как работают эти политики выполнения

Политика выполнения указывает, какую стратегию автоматического распараллеливания необходимо использовать при вызове стандартных алгоритмов.

Следующие три типа политик существуют в пространстве имен

std::execution
(табл. 9.1).

Политики выполнения подразумевают конкретные ограничения. Чем они строже, тем больше мер по распараллеливанию можно позволить:

□ все элементы функций доступа, используемые параллелизованными алгоритмами, не должны вызывать взаимных блокировок и гонок;

□ в случае параллелизации и векторизации все функции получения доступа не должны использовать блокирующую синхронизацию.


До тех пор, пока подчиняемся этим правилам, мы не столкнемся с ошибками, которые могут появиться в параллельных версиях алгоритмов STL.


 Обратите внимание: правильное использование параллельных алгоритмов STL не всегда гарантирует ускорение работы. В зависимости от того, какую задачу мы пытаемся решить, ее размера, эффективности наших структур и других методов доступа, измеряемое ускорение будет значительно различаться или даже и вовсе не произойдет. Многопроцессорная обработка — это все еще довольно сложно.


Что означает понятие «векторизация»

Векторизация — это свойство, которое должны поддерживать как процессор, так и компилятор. Кратко рассмотрим простой пример, чтобы понять суть векторизации и как она работает. Допустим, нужно сложить числа, находящиеся в очень большом векторе. Простая реализация данной задачи может выглядеть так:


std::vector v {1, 2, 3, 4, 5, 6, 7 /*...*/};


int sum {std::accumulate(v.begin(), v.end(), 0)};


Компилятор в конечном счете сгенерирует цикл из вызова

accumulate
, который может выглядеть следующим образом:


int sum {0};

for (size_t i {0}; i < v.size(); ++i) {

  sum += v[i];

}


С этого момента при разрешенной и включенной векторизации компилятор может создать следующий код. Цикл выполняет четыре шага сложения в одной итерации цикла, что сокращает количество итераций в четыре раза. Для простоты пример не работает с остатком, если вектор не содержит

N*4
элементов:


int sum {0};

for (size_t i {0}; i < v.size() / 4; i += 4) {

  sum += v[i] + v[i+1] + v[i + 2] + v[i + 3];

}

// если операция v.size()/4 имеет остаток,

// в реальном коде также нужно это обработать.


Зачем это делать? Многие процессоры предоставляют инструкции, которые могут выполнять математические операции наподобие

sum += v[i]+v[i+1]+v[i+2]+v[i+3];
всего за один шаг. Сжатие большого количества математических операций в минимальное количество инструкций — наша цель, поскольку это ускоряет программу.

Автоматическую векторизацию выполнять сложно, поскольку компилятору нужно в некоторой степени понимать нашу программу, чтобы ускорить ее, не нарушая правильности. По крайней мере помочь компилятору можно, используя стандартные алгоритмы максимально часто, поскольку компилятору проще понять их, чем запутанные циклы со сложными зависимостями.

Приостанавливаем программу на конкретный промежуток времени

Простая и удобная возможность управления потоками добавлена в С++11. В данной версии появилось пространство имен

this_thread
, содержащее функции, которые влияют только на вызывающий поток. Оно включает две разные функции, позволяющие приостановить поток на определенный промежуток времени, это позволяет перестать использовать внешние библиотеки или библиотеки, зависящие от операционной системы.

В этом примере мы сконцентрируемся на том, как приостанавливать потоки на определенный промежуток времени.


Как это делается

В этом примере мы напишем короткую программу, которая приостанавливает основной поток на определенные промежутки времени.


1. Сначала включим все необходимые заголовочные файлы и объявим об использовании пространств имен

std
и
chrono_literals
. Второе пространство содержит удобные аббревиатуры для описания промежутков времени:


#include 

#include 

#include 


using namespace std;

using namespace chrono_literals;


2. Сразу же приостановим основной поток на 5 секунд 300 миллисекунд. Благодаря пространству имен

chrono_literals
можем выразить эти промежутки времени в читабельном формате:


int main()

{

  cout << "Going to sleep for 5 seconds"

          " and 300 milli seconds.\n";

  this_thread::sleep_for(5s + 300ms);


3. Последним выражением приостановки являлось

relative
. Кроме того, можно выразить запросы
absolute
на приостановку. Приостановим поток на 3 секунды, начиная с текущего момента:


  cout << "Going to sleep for another 3 seconds.\n";

  this_thread::sleep_until(

    chrono::high_resolution_clock::now() + 3s);


4. Перед завершением программы выведем на экран какое-нибудь сообщение, что укажет на окончание второго периода приостановки.


  cout << "That's it.\n";

}


5. Компиляция и запуск программы дадут следующие результаты. В Linux, Mac и других UNIX-подобных операционных системах имеется команда

time
, принимающая другую команду, чтобы выполнить ее и определить время, которое требуется на ее выполнение. Запуск нашей программы с помощью команды
time
показывает: она работала 8,32 секунды, это значение примерно равно 5,3 и 3 секундам, на которые мы приостанавливали программу. При запуске программы можно определить промежуток времени между появлением строк, выводимых на консоль:


$ time ./sleep

Going to sleep for 5 seconds and 300 milli seconds.

Going to sleep for another 3 seconds.

That's it.

real 0m8.320s

user 0m0.005s

sys  0m0.003s


Как это работает

Функции

sleep_for
и
sleep_until
появились в версии C++11 и находятся в пространстве имен
std::this_thread
. Они блокируют выполнение текущего потока (но не процесса или программы) на конкретный промежуток времени. Поток не потребляет время процессора на протяжении блокировки. Он просто помещается операционной системой в неактивное состояние. ОС, конечно же, напоминает себе о необходимости возобновить поток. Самое лучшее заключается в том, что нам не придется волноваться, в какой операционной системе запущена программа, поскольку эту информацию от нас абстрагирует STL.

Функция

this_thread::sleep_for
принимает значение типа
chrono::duration
. В простейшем случае это просто
1s
или
5s+300ms
, как это было показано в нашем примере кода. Чтобы получить возможность применять такие удобные литералы, нужно объявить об использовании пространства имен
std::chrono_literals;
.

Функция

this_thread::sleep_until
принимает значение типа
chrono::time_point
вместо промежутка времени. Это удобно в том случае, если нужно приостановить поток до наступления конкретного момента времени.

Точность момента пробуждения зависит от операционной системы. Она будет довольно высокой для большинства ОС, но могут возникнуть проблемы, если требуется точность вплоть до наносекунд.