В Visual Studio деструктор переменных thread_local не вызывается при использовании с std :: async, это ошибка?

Следующий код

#include <iostream>
#include <future>
#include <thread>
#include <mutex>

std::mutex m;

struct Foo {
    Foo() {
        std::unique_lock<std::mutex> lock{m};
        std::cout <<"Foo Created in thread " <<std::this_thread::get_id() <<"\n";
    }

    ~Foo() {
        std::unique_lock<std::mutex> lock{m};
        std::cout <<"Foo Deleted in thread " <<std::this_thread::get_id() <<"\n";
    }

    void proveMyExistance() {
        std::unique_lock<std::mutex> lock{m};
        std::cout <<"Foo this = " << this <<"\n";
    }
};

int threadFunc() {
    static thread_local Foo some_thread_var;

    // Prove the variable initialized
    some_thread_var.proveMyExistance();

    // The thread runs for some time
    std::this_thread::sleep_for(std::chrono::milliseconds{100}); 

    return 1;
}

int main() {
    auto a1 = std::async(std::launch::async, threadFunc);
    auto a2 = std::async(std::launch::async, threadFunc);
    auto a3 = std::async(std::launch::async, threadFunc);

    a1.wait();
    a2.wait();
    a3.wait();

    std::this_thread::sleep_for(std::chrono::milliseconds{1000});        

    return 0;
}

Скомпилированная и запущенная ширина clang в macOS:

clang++ test.cpp -std=c++14 -pthread
./a.out

Получил результат

Foo Created in thread 0x70000d9f2000
Foo Created in thread 0x70000daf8000
Foo Created in thread 0x70000da75000
Foo this = 0x7fd871d00000
Foo this = 0x7fd871c02af0
Foo this = 0x7fd871e00000
Foo Deleted in thread 0x70000daf8000
Foo Deleted in thread 0x70000da75000
Foo Deleted in thread 0x70000d9f2000

Скомпилирован и запущен в Visual Studio 2015 Update 3:

Foo Created in thread 7180
Foo this = 00000223B3344120
Foo Created in thread 8712
Foo this = 00000223B3346750
Foo Created in thread 11220
Foo this = 00000223B3347E60

Деструктор не вызывается.

Это ошибка или некоторая неопределенная серая зона?

PS

Если sleep std::this_thread::sleep_for(std::chrono::milliseconds{1000}); в конце не достаточно длинный, иногда вы можете не видеть все 3 сообщения "Удалить".

При использовании std::thread вместо std::async деструкторы вызывают на обеих платформах, и все 3 сообщения "Удалить" всегда будут напечатаны.

Ответы

Ответ 1

Вступительное замечание: теперь я узнал об этом намного больше и поэтому переписал свой ответ. Спасибо @super, @MM и (последним) @DavidHaim и @NoSenseEtAl за то, что поставили меня на правильный путь.

tl; dr реализация Microsoft std::async несоответствует, но у них есть свои причины, и то, что они сделали, действительно может быть полезно, как только вы это правильно поймете.

Для тех, кто этого не хочет, не сложно записать замену замены для std::async которая работает одинаково на всех платформах. Я разместил его здесь.

Edit: Вау, как открытые MS находятся в эти дни, мне это нравится, см. Https://github.com/MicrosoftDocs/cpp-docs/issues/308


Позвольте быть в начале. cppreference имеет это сказать (акцент и зачеркнуть мое):

Функция async шаблона выполняет функцию f асинхронно (возможно, необязательно в отдельном потоке, который может быть частью пула потоков).

Тем не менее, стандарт C++ говорит следующее:

Если launch::async задан в policy, [ std::async ] вызывает [функцию f], как если бы в новом потоке выполнения...

Итак, что правильно? Эти два утверждения имеют очень разную семантику, как обнаружил ОП. Ну, конечно, стандарт верен, так как и clang, и gcc show, так почему же реализация Windows отличается? И, как и многие вещи, это сводится к истории.

(Старинная) ссылка, которую выровняли MM, имеет это, среди прочего:

... У Microsoft есть реализация [ std::async ] в виде PPL (Parallel Pattern Library)... [и] Я могу понять стремление этих компаний к изгибу правил и сделать эти библиотеки доступными через std::async, особенно если они могут значительно повысить производительность...

... Microsoft захотела изменить семантику std::async при вызове с помощью launch_policy::async. Я думаю, что это было в значительной степени исключено в последующем обсуждении... (следует логическое обоснование, если вы хотите узнать больше, а затем прочитать ссылку, это того стоит).

И PPL основан на встроенной поддержке Windows для ThreadPools, поэтому @super был прав.

Итак, что делает пул потоков Windows и для чего он хорош? Ну, он предназначен для эффективного управления часто выполняемыми, краткосрочными задачами эффективным образом, так что точка 1 не злоупотребляет, но мои простые тесты показывают, что если это ваш прецедент, то он может предложить значительную эффективность. Это, по сути, две вещи

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

Поэтому, зная все это, мы теперь можем объяснить наблюдения ОП:

  1. Создается новый поток для каждой из трех задач, запущенных main() (потому что ни одно из них не завершается немедленно).

  2. Каждый из этих трех потоков создает новую локальную переменную Foo some_thread_var.

  3. Эти три задачи выполняются до завершения, но потоки, в которых они работают, остаются в силе (сон).

  4. Затем программа засыпает на короткое время, а затем завершает работу, оставляя 3 ните-локальные переменные неразрушаемыми.

Я провел несколько тестов, и в дополнение к этому я нашел несколько ключевых моментов:

  • Когда поток перерабатывается, локальные переменные потока повторно используются. В частности, они не уничтожаются, а затем воссоздаются (вы были предупреждены!).
  • Если все асинхронные задачи завершены и вы достаточно долго ждете, пул потоков завершает все связанные потоки, а затем локальные переменные потока уничтожаются. (Без сомнения, фактические правила более сложны, чем то, что я наблюдал).
  • По мере ввода новых асинхронных задач пул потоков ограничивает скорость создания новых потоков в надежде, что он станет бесплатным, прежде чем ему понадобится выполнить все эти работы (создание новых потоков является дорогостоящим). Поэтому вызов std::async может занять некоторое время (до 300 мс в моих тестах). Тем временем, он просто висит вокруг, надеясь, что его корабль войдет. Это поведение задокументировано, но я называю это здесь, если он вас удивит.

Выводы:

  1. Реализация std::async для Microsoft является несоответствующей, но она четко разработана с определенной целью, и эта цель заключается в том, чтобы эффективно использовать API WinPool Win32. Вы можете избивать их за благосклонно пренебрегая стандартом, но так долго, и у них, вероятно, есть (важные!) Клиенты, которые полагаются на него. Я попрошу их назвать это в своей документации. Не делать это преступно.

  2. Это не безопасно использовать thread_local переменных в std::async задач на Windows. Просто не делай этого, это закончится слезами.

Ответ 2

Похоже, что это еще одна из многих ошибок в VC++. Рассмотрите эту цитату из n4750

Все переменные, объявленные с помощью ключевого слова thread_local, имеют длительность хранения потоков. Хранилище для этих объектов должно длиться в течение потока, в котором они созданы. В потоке есть отдельный объект или ссылка, а использование объявленного имени относится к объекту, связанному с текущим потоком. 2 Переменная с длительностью хранения потоков должна быть инициализирована до ее первого использования odr (6.2) и, если она построена, должна быть уничтожена при выходе потока.

+this

Если реализация выбирает политику запуска :: async, - (5.3) вызов функции ожидания в асинхронном возвращенном объекте, который разделяет общее состояние, созданное этим асинхронным вызовом, должен блокироваться до тех пор, пока соответствующий поток не будет завершен, как если бы он был присоединен, или другой тайм-аут (33.3.2.5);

Я мог ошибаться ("thread exit" vs "thread completed", но я считаю, что это означает, что переменные thread_local необходимо уничтожить до того, как.wait() вызовет разблокировку.