Подтвердить что ты не робот

Является ли реализация std:: async на Visual С++ с использованием пула потоков

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 он не делает.


Похожие вопросы:

4b9b3361

Ответ 1

(Я игнорирую специфику тега С++ 11 здесь.) Ситуация была немного разъяснена в С++ 17 P0296R2. Если Visual С++ не документирует, что его потоки не обеспечивают параллельные гарантии прогресса вперед (что было бы вообще нежелательно), ограниченный пул потоков не соответствует (в С++ 17).

Замечание о "приоритетах, связанных с внешним потоком", было удалено, возможно, потому, что среда всегда может препятствовать прогрессу программы на С++ (если не по приоритету, а затем путем приостановки, а если нет, затем сбой питания или аппаратный отказ).

В этом разделе есть один оставшийся нормативный "должен", но он относится к (как conio упоминается) только для операций блокировки, которые могут быть отложены на неопределенный срок частый параллельный доступ другого потока к одной и той же строке кэша (а не только одна и та же атомная переменная). (Я думаю, что в некоторых реализациях это может произойти, даже если другие потоки только читают.)