Используйте `async()` для порождения конкурентных задач
Причина
Подобно R.12, который рекомендует вам избегать сырых владеющих указателей, вы должны также избегать сырых потоков и сырых обещаний где возможно. Используйте функцию-фабрику такую как std::async, которая обрабатывает порождение или повторное использование потока без предоставления сырых потоков вашему коду.
Пример
int read_value(const std::string& filename)
{
std::ifstream in(filename);
in.exceptions(std::ifstream::failbit);
int value;
in >> value;
return value;
}
void async_example()
{
try {
std::future<int> f1 = std::async(read_value, "v1.txt");
std::future<int> f2 = std::async(read_value, "v2.txt");
std::cout << f1.get() + f2.get() << '\n';
} catch (const std::ios_base::failure& fail) {
// обработать исключение здесь
}
}
Примечание
К сожалению, std::async не идеален. Например, он не использует thread pool, что означает, что он может терпеть неудачу из-за истощения ресурсов, вместо постановки ваших задач в очередь для последующего выполнения. Однако, даже если вы не можете использовать std::async, вы должны предпочесть писать свою собственную функцию-фабрику, возвращающую future, нежели использовать сырые обещания.
Плохой пример
Этот пример показывает два разных способа добиться успеха в использовании std::future, но потерпеть неудачу в избежании управления сырыми std::threadами.
void async_example()
{
std::promise<int> p1;
std::future<int> f1 = p1.get_future();
std::thread t1([p1 = std::move(p1)]() mutable {
p1.set_value(read_value("v1.txt"));
});
t1.detach(); // зло
std::packaged_task<int()> pt2(read_value, "v2.txt");
std::future<int> f2 = pt2.get_future();
std::thread(std::move(pt2)).detach();
std::cout << f1.get() + f2.get() << '\n';
}
Хороший пример
Этот пример показывает один способ, которым вы могли бы следовать общей схеме, установленной std::async, в контексте где std::async сам по себе был неприемлем для использования в production.
void async_example(WorkQueue& wq)
{
std::future<int> f1 = wq.enqueue([]() {
return read_value("v1.txt");
});
std::future<int> f2 = wq.enqueue([]() {
return read_value("v2.txt");
});
std::cout << f1.get() + f2.get() << '\n';
}
Любые потоки, порождённые для выполнения кода read_value, скрыты за вызовом WorkQueue::enqueue. Пользовательский код имеет дело только с объектами future, никогда с сырыми объектами thread, promise или packaged_task.
Применение
???
CP.vec: Vectorization
Векторизация — это техника для выполнения множества задач одновременно без введения явной синхронизации. Операция просто применяется к элементам структуры данных (вектор, массив и т.д.) параллельно. Векторизация имеет интересное свойство часто не требующее нелокальных изменений в программе. Однако векторизация работает лучше всего с простыми структурами данных и алгоритмами, специально разработанными для её включения.
Резюме правил векторизации:
- ???
- ???
CP.free: Lock-free programming
Синхронизация с использованием mutexов и condition_variableов может быть относительно дорогой. Кроме того, это может привести к deadlock'у. Для производительности и исключения возможности deadlock'а нам иногда приходится использовать хитрые низкоуровневые инструменты «lock-free», которые полагаются на кратковременное получение эксклюзивного («атомного») доступа к памяти. Lock-free программирование также используется для реализации более высокоуровневых механизмов параллелизма, таких как threadы и mutexы.
Резюме правил lock-free программирования:
- CP.100: Не используйте lock-free программирование если вам это абсолютно необходимо
- CP.101: Не доверяйте вашей аппаратной части/комбинации компилятора
- CP.102: Внимательно изучайте литературу
- как/когда использовать atomics
- избегайте голодания
- используйте lock-free структуру данных, а не hand-crafted конкретный lock-free доступ
- CP.110: Не пишите собственную double-checked блокировку для инициализации
- CP.111: Используйте обычный паттерн если вам действительно нужна double-checked блокировка
- как/когда сравнивать и swapping