LINUX.ORG.RU

корректное завершение многотредовых с++ приложений?

 , , , ,


0

4

какой лучший способ внезапно прервать приложение

так, чтобы вызывались все деструкторы и все треды килялись

я пробовал элементарное exit(1)

но оно вроде не вызывает никакие деструкторы, что не есть гуд

хочется что-то более правильное

может в std:: что-то такое есть

★★★★★

Последнее исправление: smilessss (всего исправлений: 2)

Ответ на: комментарий от rupert

Деструкторы вызывать не надо, они могут воспрепятствовать внезапному прерыванию.

ну мне не так уже чтобы очень срочно, мне скорее более правильно и безследно)

но спасибо, мне кажется std::terminate всяко лучше чем exit(1)

smilessss ★★★★★
() автор топика
Последнее исправление: smilessss (всего исправлений: 2)
Ответ на: комментарий от rupert

Имеется сильно больше одного способа «принудительно выйти», но ни один из них не предполагает раскручивание стека в форкнутых потоках. Да Вы и без меня это знаете…

bugfixer ★★★★
()
Ответ на: комментарий от rumgot

Да везде она решается +- одинаково: посылаешь всем потокам сигнал (выставляя флаг) на завершение, потом join() (ждешь их завершения.
В Java еще есть механизм InterruptedException, который получает поток сидящий в sleep() или блокировке.

urxvt ★★★★★
()
Ответ на: комментарий от bugfixer

смысле чего? если exitgroup, то именно чтобы atexit пропустить, иначе дэдлоки и вся фигня. А так да,флаги, джойны и прочая. Жаль не всегда работает, особенно если изначально не было задизайнено.

Кмк реальная необходимость внезапного завершения обычна вызвана асинхронными событиями (sigint, deadline) и тогда нужно просто гарантировано умирать.

phoenix ★★★★
()

https://en.cppreference.com/w/cpp/thread/stop_token
https://en.cppreference.com/w/cpp/thread/jthread

С «внезапно» действительно не очень вяжется, но если хочется graceful shutdown, то оно.

С системными API оно, правда, дружит никак (да и внутри стандартной библиотеки единственный вид ожидания, который так прерывается — condition_variable_any::wait), какие-то попытки скрестить ежа с ужом существуют, но не знаю насколько оно production ready.

Альтернатива — pthread_cancel, в принципе это рабочий подход, если реализация pthread раскручивает стек (в musl вроде не так) и код готов к тому, что из довольно неожиданных могут вылетать исключения, которые нельзя не пробросить дальше (в частности, если зовёшь что-то содержащее cancellation point в noexcept функции — например, делаешь close() в деструкторе — ты проиграл).

Softwayer ★★
()
Последнее исправление: Softwayer (всего исправлений: 1)
Ответ на: комментарий от phoenix

таки да

_exit/ выглядит ещё привлекательнее, ведь у него нет EXIT_SUCCESS и EXIT_FAILURE

нам FAILURE здесь совсем не нужен)

smilessss ★★★★★
() автор топика
Последнее исправление: smilessss (всего исправлений: 1)

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

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

alysnix ★★★
()

Увы, но в коде каждой нити должен быть явно(!) продуман код её завершения. Если это не было учтено, то ничто тебе не даст гарантий, никакие ухищрения не помогут.

В итоге: либо мы для каждой нити продумываем механизм её завершения, либо грубое завершение работы «наше всё».

pathfinder ★★★★
()
Ответ на: комментарий от annulen

хотя я вот тут почитал

exit(1)


оно не просто киляет всё, а ещё чистит буферы и выглядит более привлекатлеьно

"terminate  \ File buffers are flushed, streams are closed, and temporary files are deleted"

smilessss ★★★★★
() автор топика
Ответ на: комментарий от yoghurt

они не текут - у нас не завершаются webrtc треды

я так понял там как-то нужно их завершать отдельно на стороне adm
и просто завершать инстанс недостаточно

в декстопе там adm на openal

и там есть код завершения всего, что тянет за собой webrtc

если комментить _data->thread.quit() - в десктопе будет такое же поведение как у нас - висящие треды webrtc которые и создают лоад и дёргают сигналинг

я вот думаю как бы у нас со стороны adm завершать эти треды, но моего скилла на это пока не хватает(


в десктопе это выглядит так


int32_t AudioDeviceOpenAL::StopRecording() {
	if (_data) {
		stopCaptureOnThread();
		_audioDeviceBuffer.StopRecording();
		if (!_data->playing) {
			_data->thread.quit();
			_data->thread.wait();
			_data = nullptr;
		}
	}
	closeRecordingDevice();
	_recordingInitialized = false;
	return 0;
}




int32_t AudioDeviceOpenAL::StopPlayout() {
	if (_data) {
		stopPlayingOnThread();
		_audioDeviceBuffer.StopPlayout();
		if (!_data->recording) {
			_data->thread.quit();
			_data->thread.wait();
			_data = nullptr;
		}
	}
	closePlayoutDevice();
	_playoutInitialized = false;
	return 0;
}

smilessss ★★★★★
() автор топика

Внутри каждого треда делать так:

while (running) {
    doJob();
}

Здесь running некая глобальная булева переменная. При завершении программы она обнуляется и ты делаешь join каждому запущенному треду.

Если треды ждут задания из какой-нибудь очереди можно ввести новый тип задания «умри», можно использовать conditional variables. Долгий I/O можно прерывать сигналами, либо использовать асинхронный I/O. Долгие вычислительные задачи разбивать на части между которыми делать проверку флага.

В общем случае невозможно корректно прервать процесс в произвольный момент времени, потому что, например, если сделать это в середине метода объекта, он может быть временно в состоянии нарушающим инвариант и деструктор просто не сработает корректно. В какие именно моменты безопасно прерываться знает только программист. ОС умеет только грохать процессы целиком, потому что при этом не важно в каком состоянии оказывается память процесса, а также ОС дожидается окончания выполнения всех системных вызовов.

KivApple ★★★★★
()
Ответ на: комментарий от KivApple

Я обычно делаю хитрее. Каждый тред хранится в каком-то экземпляре класса. Что-то вроде:

class FileDownloader {
    std::thread m_thread;
    std::atomic_flag m_running;

public:
    ...

    ~FileDownloader() {
        m_running.clear();
        m_running.notify_one(); // Если используется wait
        m_thread.join();
    }

};

Соответственно, уничтожая экземпляр класса я автоматически завершаю поток (разумеется, в функции потока есть регулярная проверка m_running в том или ином виде). У меня нет ни одного моего треда, который бы не имел владельца в виде объекта. Соответственно, главный тред в том или ином виде уничтожит всех владельцев (явным delete, либо через автоматическое уничтожение при выходе из области определения) и в конце будет только один мой тред. Плюс такого подхода по сравнению с глобальной переменной заключается в том, что я могу контролировать порядок завершения тредов, а также механизм обобщается с завершения при выходе на завершение тредов в середине работы приложения (например, допустим, юзер нажал кнопку отмены закачки - обработчик этой кнопки просто сделает delete экземпляру FileDownloader, при этом остальные треды продолжат работу, ведь юзер не закрыл приложение).

KivApple ★★★★★
()
Последнее исправление: KivApple (всего исправлений: 2)
Ответ на: комментарий от KivApple

Ещё в случае I/O можно сделать close дескриптору файла, с которым работает поток (это можно делать из любого потока). На Linux это автоматически выбивает заблокировавшийся read/write с ошибкой, которую нужно обработать прерыванием цикла потока. В этом частном случае отдельный флаг running не обязателен.

KivApple ★★★★★
()
Последнее исправление: KivApple (всего исправлений: 1)
Ответ на: комментарий от KivApple

Пояснение почему потоки нельзя завершать в произвольный момент времени.

Представь себе функцию удаления элемента из двунаправленного кольцевого связанного списка (на его месте мог бы быть обычный связанный список, просто у двунаправленного кольцевого самый простой алгоритм удаления элемента, а я ленивый):

void remove(ListItem **head, ListItem *item) {
    item->next->prev = item->prev;
    item->prev->next = item->next;
    if (item == *head) {
        *head = item->next;
        if (item == *head) {
            *head = nullptr;
        }
    }
}

Если прервать эту функцию в любой момент времени между началом и концом, то список будет в некорректном состоянии и код, который бы освобождал его память, потенциально сработал бы некорректно.

Или, например, если поток захватил mutex и выполняет какие-то манипуляции с общими данными. Mutex как раз потому и нужен, потому что если другие треды увидят незаконченные модификации, то сойдут с ума. Так что нельзя просто освободить все mutex при убийстве потока. А если их не освободить, то будет dead lock. Ждать освобождения mutex? Это может занять много времени, плюс может быть алгоритм, который постоянно захватывает разные mutex и в каждый момент времени владеет хоть одним.

KivApple ★★★★★
()
Последнее исправление: KivApple (всего исправлений: 2)
Ответ на: комментарий от bugfixer

А раскручивать стек бесполезно. Потому что часть объектов могут быть в состоянии нарушающим инвариант. И их деструкторы к этому не будут готовы.

Программист обычно более-менее знает какие строчки кода могут выбросить эксепшн и предпринимать меры, чтобы в эти моменты все объекты были в адекватном состоянии. А такое что вообще ЛЮБАЯ строчка может быть прервана, к этому никто не может быть готов (если не полагаться на полное освобождение памяти процесса уже со стороны ОС, при этом сама ОС не даёт жёстко завершать свои системные вызовы, например, потому что глобальное состояние ядра делится между всеми процессами).

Даже всякие джавы могут только память за тобой подчистить, но привести к инварианту общие структуры разделяемые mutex-ом уже нет, поэтому InterruptedException прерывает только строго ограниченный набор библиотечных функций (и ты должен это как-то обработать, если надо), а в произвольном месте тред прервать нельзя даже в управляемом языке.

KivApple ★★★★★
()
Последнее исправление: KivApple (всего исправлений: 2)

Какого-то автоматического способа не знаю, но, по-моему, удобно заюзать исключения

#include <thread>
#include <atomic>
#include <chrono>
using namespace std;
using namespace std::chrono;

std::atomic_flag stop;
struct Completion_exc {};
void check_flag() {
    if (stop.test())
        throw Completion_exc{};
}

void thread_fn() try
{
    while (true)
        while (true)
            while (true) {
                this_thread::sleep_for(1s);
                check_flag();
            }
}
catch (Completion_exc &) {}

int main() {
    jthread t(thread_fn);
    this_thread::sleep_for(1s);
    stop.test_and_set();
}

Удобство в том, что не надо вкостыливать вереницу проверок флага в каждом внешнем блоке вроде старшего while, мы просто чекаем в самом «сердце» зацикенной логики и не паримся о деталях. Конечно это вариация на тему «проверка флага», но это удобней топорной реализации.

PS: вместо atomic_flag лучше заюзать std::stop_token, его можно передавать в std’шные wait функции.

kvpfs ★★
()
Последнее исправление: kvpfs (всего исправлений: 1)

Вешаешь обработчик sigterm, по обработчику выставляешь флаг, увидев который все треды начинают корректно завершаться, тех кто не завершился корректно, отдельный наблюдатель должен пристрелить.

А после определённого таймаута на вот это вот завершение приложению слать sigkill, который уже добивает выживших, буде такие остаются.

Можно ещё добивать через sigkill на PGID, но это если процесс не только треды стартует, но ещё и форкается.

Dark_SavanT ★★★★★
()
Ответ на: комментарий от Dark_SavanT

я думаю придумывать велосипед смысла нет

у меня есть точка в софте

до которой доходит работа

а затем я просто вызываю exit(1)

и башем перезапускаю бинарник

а всё из-за того, что я пока не могу найти как завершить треды которые были вызваны работой сторонних webrtc либ

smilessss ★★★★★
() автор топика
Ответ на: комментарий от smilessss

а затем я просто вызываю exit(1) и башем перезапускаю бинарник

Знатная костылина, уж лучше форкнуться и после завершения чайлда форкаться заново, в цикле. Плюс можно какими-то данными обменяться. То же самое, но без баша.

kvpfs ★★
()
Ответ на: комментарий от kvpfs

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

по хорошему нужно бы завершать эти вебртс треды, но как пока никто не придумал

smilessss ★★★★★
() автор топика

забавно, тс год назад искал исполнителя на 2к подпилить с++ проект, теперь четвертый месяц бегает по лору и ищет как таки выключить вязанку тредов и не охуеть, да, лор технический форум прям техника изо всех щелей

hizel ★★★★★
()
Ответ на: комментарий от yoghurt

ко мне пришла гениальная идея

я взял список тредов которые висят

rtc-low-prio
TaskQueuePacedS
rtp_send_contro
ModuleProcessTh
AudioEncoder
DecodingQueue
IncomingVideoSt


это всё внутри webrtc судя по всему

грепом сейчас буду проходиться по файлам
искать откуда оно берётся


rtc-low-prio например это webrtc_voice_engine.cc

https://imgur.com/5WHMfMY.png

и буду пробовать туда прям добавлять код явного их завершения

по идее может же сработать )

smilessss ★★★★★
() автор топика
Ответ на: комментарий от yoghurt

хотя вообще я думаю мы просто не сделаем как там

там я вижу при стопе такие чудеса

alSourceStop(_data->source);
			unqueueAllBuffers();
			alDeleteBuffers(_data->buffers.size(), _data->buffers.data());
			alDeleteSources(1, &_data->source);



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

smilessss ★★★★★
() автор топика
Ответ на: комментарий от smilessss

заметил все эти треды - создаются неким task_queue_factory

может есть какой-то способ стопнуть все эти task_queue ?)

task_queue_(task_queue_factory->CreateTaskQueue(
          "TaskQueuePacedSender"

task_queue_factory_->CreateTaskQueue(
          "rtc-low-prio", 

task_queue_(task_queue_factory->CreateTaskQueue(
          "rtp_send_controller",

smilessss ★★★★★
() автор топика
Последнее исправление: smilessss (всего исправлений: 1)