В главе 5.3.5.3 учебника "Язык программирования С++" (4-е издание) Бьярне Страуструп пишет о функции std::async
.
Существует очевидное ограничение: даже не думайте использовать
async()
для задач, которые обмениваются ресурсами, требующими блокировки - с помощьюasync()
вы даже не знаете, сколькоthread
будет использоваться, потому что доasync()
, чтобы решить, на основе того, что он знает о системных ресурсах, доступных во время вызова.
Аналогичное наставление можно найти в С++ 11-FAQ на своем веб-сайте.
"Простой" является наиболее важным аспектом дизайна
async()
/future
; фьючерсы также могут использоваться с потоками в целом, но даже не думают использоватьasync()
для запуска задач, которые выполняют I/O, манипулируют мьютексами или другими способами взаимодействуют с другими задачами.
Интересно, что он не уточняет это ограничение, когда он более подробно возвращается к возможностям С++ 11 concurrency в § 42.4.6 своей книги. Еще более интересно, что в этой главе он фактически продолжает (по сравнению с выражением на своем сайте):
Простым и реалистичным использованием
async()
было бы создание задачи для сбора ввода от пользователя.
Документация async
на cppreference.com
не содержит никаких таких ограничений вообще.
После прочтения некоторых предложений и обсуждений, которые приводят к стандарту С++ 11 в его окончательной форме (к сожалению, у меня нет доступа), я понимаю, что async
был включен очень поздно в С++ 11 стандарт, и было много дискуссий о том, насколько мощной должна быть эта функция. В частности, я нашел статью Async Tasks в С++ 11: не совсем там Bartosz Milewski - очень хорошее резюме проблем, которые необходимо учитывать при реализации async
.
Однако все обсуждаемые проблемы связаны с переменными thread_local
, которые не разрушаются, если поток перерабатывается, ложные взаимоблокировки или нарушения доступа к данным, если поток переключает свою задачу в середине действия, и обе задачи содержат mutex
или recursive_mutex
соответственно и так далее. Это серьезная проблема для разработчиков этой функции, но если я правильно понимаю, текущая спецификация async
требует, чтобы все эти детали были скрыты от пользователя, выполнив задачу либо в потоке вызывающего, либо как если бы новый поток был создан для задача.
Итак, мой вопрос: Что мне не разрешено делать с async
, что мне разрешено использовать с помощью thread
вручную и в чем причина этого ограничения?
Например, что-то не так в следующей программе?
#include <future>
#include <iostream>
#include <mutex>
#include <vector>
static int tally {};
static std::mutex tally_mutex {};
static void
do_work(const int amount)
{
for (int i = 0; i < amount; ++i)
{
// Might do something actually useful...
const std::unique_lock<std::mutex> lock {tally_mutex};
tally += 1;
}
}
int
main()
{
constexpr int concurrency {10};
constexpr int amount {1000000};
std::vector<std::future<void>> futures {};
for (int t = 0; t < concurrency; ++t)
futures.push_back(std::async(do_work, amount / concurrency));
for (auto& future : futures)
future.get();
std::cout << tally << std::endl;
}
Очевидно, что если среда выполнения решит запланировать все задачи в основном потоке, мы бесполезно будем приобретать мьютекс снова и снова без уважительной причины. Но, хотя это может быть неэффективным, это неверно.