Каким ограничением std:: async является Stroustrup?

В главе 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;
}

Очевидно, что если среда выполнения решит запланировать все задачи в основном потоке, мы бесполезно будем приобретать мьютекс снова и снова без уважительной причины. Но, хотя это может быть неэффективным, это неверно.

Ответы

Ответ 1

"Проблема" с std:: async заключается в том, что по умолчанию вы не знаете, запускает ли он поток. Если ваша функция должна запускаться в отдельном потоке, это проблема, так как ваша функция может не работать до вызова функции get() или wait(). Вы можете передать std:: launch:: async, чтобы гарантировать, что функция запускается в собственном потоке, который похож на std:: thread, который не может отсоединиться BG.

Ответ 2

Вы указываете проблему. Нитки могут быть переработаны... Поэтому любое использование хранилища thread_local опасно.

Ваша петля справедлива - но, как вы сказали, может быть неэффективной.

Вы можете потребовать, чтобы язык создавал другой поток, используя std:: launch:: async. Но потоки все равно могут быть переработаны.