{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.