C++17 STL Стандартная библиотека шаблонов - Яцек Галовиц
Шрифт:
Интервал:
Закладка:
print_distro(std::bernoulli_distribution{0.75}, samples);
cout << "discrete_distributionn";
print_distro(discrete_distribution<int>{{1, 2, 4, 8}}, samples);
12. Существует множество других генераторов распределений. Они полезны только в очень специфических ситуациях. Если вы никогда о них не слышали, то они, возможно, вам и не нужны. Однако, поскольку наша программа создает «аккуратные» гистограммы, показывающие распределение, из интереса выведем их все:
cout << "binomial_distributionn";
print_distro(binomial_distribution<int>{10, 0.3}, samples);
cout << "negative_binomial_distributionn";
print_distro(negative_binomial_distribution<int>{10, 0.8}, samples);
cout << "geometric_distributionn";
print_distro(geometric_distribution<int>{0.4}, samples);
cout << "exponential_distributionn";
print_distro(exponential_distribution<double>{0.4}, samples);
cout << "gamma_distributionn";
print_distro(gamma_distribution<double>{1.5, 1.0}, samples);
cout << "weibull_distributionn";
print_distro(weibull_distribution<double>{1.5, 1.0}, samples);
cout << "extreme_value_distributionn";
print_distro(extreme_value_distribution<double>{0.0, 1.0}, samples);
cout << "lognormal_distributionn";
print_distro(lognormal_distribution<double>{0.5, 0.5}, samples);
cout << "chi_squared_distributionn";
print_distro(chi_squared_distribution<double>{1.0}, samples);
cout << "cauchy_distributionn";
print_distro(cauchy_distribution<double>{0.0, 0.1}, samples);
cout << "fisher_f_distributionn";
print_distro(fisher_f_distribution<double>{1.0, 1.0}, samples);
cout << "student_t_distributionn";
print_distro(student_t_distribution<double>{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::async;
□ реализация небольшой автоматической библиотеки для распараллеливания с использованием 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.