В С++ у меня есть std::vector потоков, каждый из которых выполняет функцию, работающую вечно [пока (true)]. Я присоединяюсь к ним в цикле for:
for (auto& thread : threads)
{
thread.join();
}
Когда программа завершается, я получаю вызов std::terminate() внутри деструктора одного из потоков. Я думаю, что понимаю, почему это происходит, за исключением первого потока, другие вызовы соединения не вызываются.
Каков правильный способ присоединения к этим потокам? И действительно ли необходимо к ним присоединяться? (при условии, что они не должны присоединяться при нормальных обстоятельствах)
Вам нужен какой-то способ остановить потоки, чтобы на самом деле их можно было объединить до выхода процесса. Если потоки работают бесконечно, они не завершатся, и вызов join
будет заблокирован.
если вы можете использовать С++ 20, есть std::jthread
с некоторым механизмом, чтобы остановить его контролируемым образом. Если нет, вы должны превратить свой while(true)
в какой-нибудь while(should_keep_running)
через условные переменные или аналогичные.
ни один поток не работает вечно. Imho while(true)
плохой дизайн, который нужно исправить
@DrewDormann причина std::terminate в том, что к потоку можно присоединиться, как видно из деструктора std::thread: cplusplus.com/reference/thread/thread/~thread
@OmerKawaz, за исключением того, что код застрял на первом join()
, поэтому нет никаких шансов, что какой-либо деструктор потока будет достигнут.
Почему потоки работают вечно? Не было бы разумно позволить им закончить, когда вы собираетесь выйти из программы. Пример
TLDR: t.join()
ждет остановки потока t
. Он не делает ничего другого. Если поток никогда не останавливается, то t.join()
никогда не вернется.
Независимо от того, что говорят ответы, не используйте отделить, если только вы не управляете завершением потока вручную другими способами.
What is the correct way of joining those threads?
Ваш способ прекрасен, в зависимости от того, что вы пытаетесь сделать.
And is it actually necessary to join them?
Да. И нет.
Видите ли, основная проблема с std::thread
заключается в том, что вам нужно очистить их, иначе они «делают плохие вещи» (ТМ), но присоединение к ним — это только один из способов их очистки. Другой способ - просто отделить их от ваших реальных потоков, если вы больше не хотите их контролировать (что, кажется, имеет место?).
Вам нужно спросить себя, имеет ли смысл ваша настройка, когда вы создаете целую кучу потоков, которые не заканчиваются чисто, а вместо этого случайным образом прерываются из-за того, что весь ваш процесс умирает. Что происходит с работой, которую они должны были делать? Если они куда-то записывают свой вывод, и он прерывается на полпути, согласны ли вы, ваши работодатели и ваши клиенты с повреждением файлов?
Если потоки не могут быть объединены, потому что они никогда не выходят, вы можете использовать std::thread::detach
(https://en.cppreference.com/w/cpp/thread/thread/detach). В любом случае, прежде чем присоединиться, вы всегда должны проверять std::thread::joinable
(https://en.cppreference.com/w/cpp/thread/thread/joinable).
std::terminate
действительно, скорее всего, из-за того, что работающий поток был уничтожен, а не был отсоединен или присоединен до этого. Однако обратите внимание, что то, что происходит с отсоединенными потоками при выходе из приложения, определяется реализацией. Если возможно, вам, вероятно, следует изменить логику в этих потоках, чтобы обеспечить плавный выход (std::jthread
или std::atomic
могут помочь сделать потоки останавливаемыми):
Обновлено: Полуполный "правильный" код С++ 17:
std::atomic stop{false};
std::vector<std::thread> threads;
threads.emplace_back(std::thread{[&] { while (!stop.load()) { /* */ }}});
threads.emplace_back(std::thread{[&] { while (!stop.load()) { /* */ }}});
//...
stop.store(true);
for (auto& thread : threads)
{
if (thread.joinable())
{
thread.join();
}
}
Полуполный "правильный" код С++ 20:
std::vector<std::jthread> threads;
threads.emplace_back(std::jthread{[] (std::stop_token stopToken) { while (!stopToken.stop_requested()) { /* */ }}});
threads.emplace_back(std::jthread{[] (std::stop_token stopToken) { while (!stopToken.stop_requested()) { /* */ }}});
C++20 std::jthread
позволяет функциям, которые принимают std::stop_token
, получать сигнал об остановке. Деструктор std::~jthread()
сначала запрашивает остановку через токен, а затем присоединяется, поэтому в приведенной выше настройке в основном не требуется ручная очистка. К сожалению, в настоящее время его поддерживают только MSVC STL и libstdc++
, а libc++
Clang — нет. Но достаточно легко реализовать себя поверх std::thread
, если вы хотите немного потренироваться.
Тем не менее, отдельные потоки открывают совершенно новую банку червей.
@TedLyngmo Действительно, отсюда мое предложение изменить логику и заставить их изящно выйти. std::jthread
приходит на ум или просто std::atomic
как сигнал к выходу из цикла.
Я бы не стал предлагать использовать detach. Это поощряет плохую практику.
@Galik Раньше я соглашался, но я сталкивался с законными вариантами использования. Например, при использовании std::cin
. Если нет входа. Его нельзя прервать/отменить и нет тайм-аута. Таким образом, даже если у вас есть изящная логика остановки, у нее никогда не будет шанса быть использованной. Единственный вариант — отсоединить и позволить ОС убить его в конце. Неблокирующий ввод-вывод только с помощью STL практически невозможен.
Вы показываете правильный способ присоединиться к темам. Вы не показываете причину
std::terminate
(это не минимальный воспроизводимый пример). Возможно, поток уже был присоединен. Возможно, вы вызываете этот код из одного из присоединяемых потоков.