c++ - tools - vld visual studio 2017




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

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

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

+ это

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

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

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

#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;
}

Скомпилируйте и запустите ширину лязга в 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, обновление 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

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

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


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

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

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

Редактировать: Ого, насколько открыты MS в эти дни, мне это нравится, смотрите: https://github.com/MicrosoftDocs/cpp-docs/issues/308

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

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

Тем не менее, стандарт C ++ говорит это:

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

Так что правильно? Эти два оператора имеют очень разную семантику, как обнаружил OP. Конечно, стандарт верен, как показывают clang и gcc, так почему же реализация 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. Реализация Microsoft std::async не соответствует требованиям, но она явно разработана с определенной целью, и эта цель заключается в том, чтобы эффективно использовать Win32 ThreadPool API. Вы можете побить их за вопиющее пренебрежение стандартом, но так было давно, и у них, вероятно, есть (важно!) Клиенты, которые полагаются на это. Я попрошу их об этом в их документации. Не делать это преступно.

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





memory-leaks