Как присоединиться к нескольким потокам, которые не останавливаются в С++

В С++ у меня есть std::vector потоков, каждый из которых выполняет функцию, работающую вечно [пока (true)]. Я присоединяюсь к ним в цикле for:

for (auto& thread : threads) 
{
    thread.join();
}

Когда программа завершается, я получаю вызов std::terminate() внутри деструктора одного из потоков. Я думаю, что понимаю, почему это происходит, за исключением первого потока, другие вызовы соединения не вызываются.

Каков правильный способ присоединения к этим потокам? И действительно ли необходимо к ним присоединяться? (при условии, что они не должны присоединяться при нормальных обстоятельствах)

Вы показываете правильный способ присоединиться к темам. Вы не показываете причину std::terminate (это не минимальный воспроизводимый пример). Возможно, поток уже был присоединен. Возможно, вы вызываете этот код из одного из присоединяемых потоков.

Drew Dormann 16.03.2022 18:19

Вам нужен какой-то способ остановить потоки, чтобы на самом деле их можно было объединить до выхода процесса. Если потоки работают бесконечно, они не завершатся, и вызов join будет заблокирован.

Some programmer dude 16.03.2022 18:19

если вы можете использовать С++ 20, есть std::jthread с некоторым механизмом, чтобы остановить его контролируемым образом. Если нет, вы должны превратить свой while(true) в какой-нибудь while(should_keep_running) через условные переменные или аналогичные.

463035818_is_not_a_number 16.03.2022 18:21

ни один поток не работает вечно. Imho while(true) плохой дизайн, который нужно исправить

463035818_is_not_a_number 16.03.2022 18:22

@DrewDormann причина std::terminate в том, что к потоку можно присоединиться, как видно из деструктора std::thread: cplusplus.com/reference/thread/thread/~thread

Omer Kawaz 16.03.2022 18:22

@OmerKawaz, за исключением того, что код застрял на первом join(), поэтому нет никаких шансов, что какой-либо деструктор потока будет достигнут.

Remy Lebeau 16.03.2022 18:30

Почему потоки работают вечно? Не было бы разумно позволить им закончить, когда вы собираетесь выйти из программы. Пример

Ted Lyngmo 16.03.2022 18:31

TLDR: t.join() ждет остановки потока t. Он не делает ничего другого. Если поток никогда не останавливается, то t.join() никогда не вернется.

Solomon Slow 16.03.2022 19:26

Независимо от того, что говорят ответы, не используйте отделить, если только вы не управляете завершением потока вручную другими способами.

Galik 16.03.2022 22:44
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
9
79
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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, если вы хотите немного потренироваться.

Тем не менее, отдельные потоки открывают совершенно новую банку червей.

Ted Lyngmo 16.03.2022 18:23

@TedLyngmo Действительно, отсюда мое предложение изменить логику и заставить их изящно выйти. std::jthread приходит на ум или просто std::atomic как сигнал к выходу из цикла.

Resurrection 16.03.2022 18:25

Я бы не стал предлагать использовать detach. Это поощряет плохую практику.

Galik 16.03.2022 22:40

@Galik Раньше я соглашался, но я сталкивался с законными вариантами использования. Например, при использовании std::cin. Если нет входа. Его нельзя прервать/отменить и нет тайм-аута. Таким образом, даже если у вас есть изящная логика остановки, у нее никогда не будет шанса быть использованной. Единственный вариант — отсоединить и позволить ОС убить его в конце. Неблокирующий ввод-вывод только с помощью STL практически невозможен.

Resurrection 17.03.2022 07:34

Другие вопросы по теме