Visual С++ использует пул потоков Windows (Vista CreateThreadpoolWork
, если он доступен, и QueueUserWorkItem
если нет) при вызове std::async
с std::launch::async
.
Количество потоков в пуле ограничено. Если вы создаете несколько задач, которые выполняются в течение длительного времени без сна (включая выполнение ввода-вывода), предстоящие задачи в очереди не будут иметь возможности работать.
Стандарт (я использую N4140) говорит, что используя std::async
с std::launch::async
... вызывает
INVOKE(DECAY_COPY(std::forward<F>(f)), DECAY_COPY(std::forward<Args>(args))...)
(20.9.2, 30.3.1.2) , как если бы в новом потоке выполнения, представленном объектом потока, при этом вызовыDECAY_COPY()
оценивались в который называетсяasync
.
(§30.6.8p3, Акцент мой.)
Конструктор std::thread
создает новый поток и т.д.
О потоках вообще говорится (§1.10p3):
Реализации должны гарантировать, что все разблокированные потоки в конечном итоге достигнут прогресса. [Примечание. Стандартные функции библиотеки могут незаметно блокировать операции ввода-вывода или блокировки. Факторы в среде исполнения, включая приоритеты, связанные с внешним потоком, могут помешать реализации определенных гарантий продвижения вперед. -end note]
Если я создаю кучу потоков ОС или std::thread
s, все из которых выполняют очень длинные (возможно, бесконечные) задачи, все они будут запланированы (по крайней мере, в Windows, не возиться с приоритетами, сродствами и т.д.), Если мы планируем те же задачи в пуле потоков Windows (или используем std::async(std::launch::async, ...)
, который делает это), более поздние запланированные задачи не будут выполняться до тех пор, пока предыдущие задачи не будут завершены.
Является ли это законным, строго говоря? И что означает "в конечном счете"?
Проблема состоит в том, что если запланированные задачи де-факто бесконечны, остальные задачи не будут выполняться. Таким образом, другие потоки (а не потоки ОС, но "С++ - потоки" в соответствии с правилом as-if) не достигнут прогресса.
Можно утверждать, что если код имеет бесконечные циклы, поведение undefined и, следовательно, оно является законным.
Но я утверждаю, что нам не нужен бесконечный цикл проблемного типа, в котором говорится, что UB делает это. Доступ к неустойчивым объектам, выполнение операций атомарного управления и синхронизации - все побочные эффекты, которые "отключают" предположение о завершении циклов.
(У меня есть куча асинхронных вызовов, выполняющих следующие lambda
auto lambda = [&] {
while (m.try_lock() == false) {
for (size_t i = 0; i < (2 << 24); i++) {
vi++;
}
vi = 0;
}
};
и блокировка освобождается только при вводе пользователя. Но есть и другие допустимые типы законных бесконечных циклов.)
Если я планирую пару таких задач, задачи, которые я планирую после них, не запускаются.
На самом деле злой пример будет запускать слишком много задач, которые будут выполняться до тех пор, пока блокировка не будет выпущена/флаг не будет поднят, а затем запланируйте с помощью `std:: async (std:: launch:: async,...) задачу, которая поднимает флаг. Если слово "в конечном счете" не означает что-то очень удивительное, эта программа должна прекратиться. Но при реализации VС++ это не будет!
Мне кажется, что это нарушение стандарта. Меня удивляет второе предложение в примечании. Факторы могут препятствовать реализации определенных гарантий продвижения вперед. Итак, как эти реализации соответствуют?
Это похоже на то, что могут быть факторы, препятствующие реализации определенных аспектов упорядочения памяти, атомарности или даже существования нескольких потоков исполнения. Отличные, но соответствующие хостинговые реализации должны поддерживать несколько потоков. Слишком плохо для них и их факторов. Если они не могут предоставить их, а не С++.
Это ослабление требования? Если это так интерпретируется, это полное изъятие требования, поскольку оно не указывает, каковы факторы и, что более важно, какие гарантии могут не предоставляться реализациями.
Если нет - что означает эта заметка?
Я помню, что сноски являются ненормативными в соответствии с Директивами ISO/IEC, но я не уверен в примечаниях. Я нашел в директивах ISO/IEC следующее:
24 Примечания
24.1 Цель или обоснование
Примечания используются для предоставления дополнительной информации, предназначенной для содействия пониманию или использованию текста документа. Документ может использоваться без примечаний.
Акцент мой. Если я рассматриваю документ без этой неясной заметки, мне кажется, что потоки должны прогрессировать, std::async(std::launch::async, ...)
имеет эффект как-еслифунктор выполняется в новом потоке, поскольку, если он создается с помощью std::thread
, и, таким образом, функторы, отправленные с использованием std::async(std::launch::async, ...)
, должны добиваться прогресса. И в реализации VС++ с помощью threadpool они этого не делают. Таким образом, VС++ нарушает стандарт в этом отношении.
Полный пример, протестированный с использованием VS 2015U3 на Windows 10 Enterprise 1607 на i5-6440HQ:
#include <iostream>
#include <future>
#include <atomic>
int main() {
volatile int vi{};
std::mutex m{};
m.lock();
auto lambda = [&] {
while (m.try_lock() == false) {
for (size_t i = 0; i < (2 << 10); i++) {
vi++;
}
vi = 0;
}
m.unlock();
};
std::vector<decltype(std::async(std::launch::async, lambda))> v;
int threadCount{};
std::cin >> threadCount;
for (int i = 0; i < threadCount; i++) {
v.emplace_back(std::move(std::async(std::launch::async, lambda)));
}
auto release = std::async(std::launch::async, [&] {
__asm int 3;
std::cout << "foo" << std::endl;
vi = 123;
m.unlock();
});
return 0;
}
С 4 или менее он завершается. С более чем 4 он не делает.
Похожие вопросы:
-
Есть ли реализация std:: async, которая использует пул потоков? - Но это не вопрос о законности и не имеет ответ в любом случае.
-
std:: async - зависимое от реализации использование? - Указывает, что "пулы потоков на самом деле не поддерживаются", но фокусируется на переменных
thread_local
(которые разрешается, даже если "не прямолинейно" или нетривиально, как говорят ответ и комментарий) и не рассматривает примечание рядом с требованием достижения прогресса.