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

{10, 0.3}, samples);

  cout << "negative_binomial_distribution\n";

  print_distro(
negative_binomial_distribution{10, 0.8}, samples);

  cout << "geometric_distribution\n";

  print_distro(geometric_distribution{0.4}, samples);

  cout << "exponential_distribution\n";

  print_distro(exponential_distribution{0.4}, samples);

  cout << "gamma_distribution\n";

  print_distro(gamma_distribution{1.5, 1.0}, samples);

  cout << "weibull_distribution\n";

  print_distro(weibull_distribution{1.5, 1.0}, samples);

  cout << "extreme_value_distribution\n";

  print_distro(
extreme_value_distribution{0.0, 1.0}, samples);

  cout << "lognormal_distribution\n";

  print_distro(lognormal_distribution{0.5, 0.5}, samples);

  cout << "chi_squared_distribution\n";

  print_distro(chi_squared_distribution{1.0}, samples);

  cout << "cauchy_distribution\n";

  print_distro(cauchy_distribution{0.0, 0.1}, samples);

  cout << "fisher_f_distribution\n";

  print_distro(fisher_f_distribution{1.0, 1.0}, samples);

  cout << "student_t_distribution\n";

  print_distro(student_t_distribution{1.0}, samples);

}


13. Компиляция и запуск программы дадут следующий результат. Сначала запустим программу с 1000 образцами для каждого распределения (рис. 8.7).


14. Еще один запуск, на этот раз с 1 000 000 образцов для каждого распределения, покажет, что гистограммы выглядят гораздо чище и более характерно для каждого из них. Кроме того, мы увидим, какие распределения генерируются медленно, а какие — быстро (рис. 8.8).



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

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

Чтобы использовать любое распределение, сначала нужно создать для него соответствующий объект. Мы видели, что разные распределения принимают разные аргументы конструктора. В описании примера мы кратко остановились на некоторых видах распределения, поскольку большинство из них слишком специфичны и/или сложны, чтобы рассматривать их здесь. Не волнуйтесь, все они подробно описаны в документации к C++ STL.

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

Программа, которую мы только что написали, позволит нам получить наиболее полную информацию о разных распределениях. В дополнение к этому рассмотрим самые важные виды распределения (табл. 8.2). Для всех остальных видов распределения вы можете обратиться к документации C++ STL.

Глава 9Параллелизм и конкурентность 

В этой главе:

□ автоматическое распараллеливание кода, использующего стандартные алгоритмы;

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

□ запуск и приостановка потоков;

□ выполнение устойчивой к исключениям общей блокировки с помощью

std::unique_lock
и
std::shared_lock
;

□ избегание взаимных блокировок с применением

std::scoped_lock
;

□ синхронизация конкурентного использования

std::cout
;

□ безопасное откладывание инициализации с помощью

std::call_once
;

□ отправка выполнения задач в фоновый режим с применением

std::async
;

□ реализация идиомы «производитель/потребитель» с использованием

std::condition_variable
;

□ реализация идиомы «несколько потребителей/производителей» с помощью

std::condition_variable
;

□ распараллеливание отрисовщика множества Мандельброта в ASCII с применением

std::a
sync;

□ реализация небольшой автоматической библиотеки для распараллеливания с использованием

std::future

Введение

До C++11 язык C++ не поддерживал параллельные вычисления. Это не значило, что запуск, управление, остановка и синхронизация потоков были невыполнимы, но для каждой операционной системы требовались специальные библиотеки, поскольку потоки по своей природе связаны с ОС.

С появлением C++11 мы получили библиотеку

std::thread
, которая позволяет управлять потоками всех операционных систем. Для синхронизации потоков в C++11 были созданы классы-мьютексы, а также удобные оболочки блокировок в стиле RAII. Вдобавок
std::condition_variable
позволяет отправлять гибкие уведомления о событиях между потоками.

Кроме того, интересными дополнениями являются

std::async
и
std::future
: теперь можно оборачивать произвольные нормальные функции в вызовы
std::async
, чтобы выполнять их асинхронно в фоновом режиме. Такие обернутые функции возвращают объекты типа
std::future
, которые обещают содержать результаты работы функции, и можно сделать что-то еще, прежде чем дождаться их появления. Еще одно значительное улучшение STL — политики выполнения, которые могут быть добавлены к 69 уже существующим алгоритмам. Это дополнение означает, что можно просто добавить один аргумент, описывающий политику выполнения, в существующие вызовы стандартных алгоритмов и получить доступ к параллелизации, не нуждаясь в переписывании сложного кода.

В данной главе мы пройдемся по всем указанным дополнениям, чтобы узнать их самые важные особенности. После этого у нас будет достаточно информации о поддержке параллелизации в STL версии C++17. Мы не станем рассматривать все свойства, только самые важные. Информация, полученная из этой книги, позволит быстро понять остальную часть механизмов распараллеливания, которую можно найти в Интернете в документации к STL версии C++17.

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

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

В C++17 появилось одно действительно крупное расширение для параллелизма: политики выполнения для стандартных алгоритмов. Шестьдесят девять алгоритмов были расширены и теперь принимают политики выполнения, чтобы работать параллельно на нескольких ядрах и даже при включенной векторизации.

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

В данном разделе мы реализуем простую программу (с не самым серьезным сценарием применения), которая генерирует несколько вызовов алгоритмов STL. При этом увидим, как легко использовать политики выполнения C++17, чтобы запустить их в нескольких потоках. В последних подразделах мы более подробно рассмотрим разные политики выполнения.


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

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


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

std
. Заголовочный файл
execution
мы еще не видели, он появился в C++17.