Лучшие практики изящного завершения работы C++

Я пишу многопоточное приложение на С++ для операционных систем *nix. Каковы некоторые рекомендации по корректному завершению такого приложения? Моя интуиция заключается в том, что я хотел бы установить обработчик сигналов на SIGINT (SIGTERM?), Который останавливает/присоединяется к моим потокам. Кроме того, можно ли «гарантировать», что все деструкторы вызываются (при условии, что при обработке сигнала не возникают другие ошибки или исключения)?


person fredbaba    schedule 28.12.2013    source источник
comment
Хороший вопрос... Мне самому интересно узнать ответ.   -  person FuzzyBunnySlippers    schedule 29.12.2013
comment
Я думаю, что неплохо использовать глобальный флаг выключения, который вы проверяете в основном цикле событий, или что-то еще, что вы обычно используете для синхронизации. Чтобы гарантировать вызов всех деструкторов, вам необходимо раскрутить стек вызовов каждого потока, предполагая, что вы используете RAII для объектов, выделенных в куче. На самом деле нет серебряной пули для этого, самым грубым было бы генерировать исключение, а затем перехватывать его в основной функции потока.   -  person Alexei Averchenko    schedule 29.12.2013


Ответы (3)


Приходят на ум некоторые соображения:

  • назначьте 1 поток, отвечающий за организацию завершения работы, например, как предложил Дитермастер, это может быть основной поток, если вы пишете автономное приложение. Или, если вы пишете библиотеку, предоставьте интерфейс (например, вызов функции), с помощью которого клиентская программа может завершать объекты, созданные в библиотеке.

  • вы не можете гарантировать вызов деструкторов; это зависит от вас и требует тщательного вызова удаления для каждого нового. Возможно, вам помогут умные указатели. Но, на самом деле, это дизайнерское соображение. Основные компоненты должны иметь семантику запуска и остановки, которую вы можете вызвать из конструктора и деструктора класса.

  • последовательность выключения для набора взаимодействующих объектов может потребовать некоторых усилий, чтобы получить ее правильно. Например, прежде чем удалить объект, вы уверены, что какой-то механизм таймера не попытается вызвать его через несколько микро/милли/секунд позже? Метод проб и ошибок — ваш друг; разработайте структуру, которая может многократно и быстро запускать и останавливать ваше приложение, чтобы выявить условия гонки, связанные с отключением.

  • сигналы — это один из способов вызвать событие; другие могут периодически опрашивать известный файл или открывать сокет и получать от него какие-то данные. В любом случае вы хотите отделить код последовательности выключения от триггерного события.

person Darren Smith    schedule 28.12.2013

Я рекомендую, чтобы основной поток закрывал все рабочие потоки перед выходом из себя. Отправьте каждому рабочему событие, сообщающее ему о необходимости очистки и выхода, и подождите, пока каждый из них сделает это. Это позволит запускать все деструкторы C++.

person Dithermaster    schedule 28.12.2013

Что касается управления сигналами, единственное, что вы можете переносимо и безопасно сделать внутри обработчика сигналов, — это записать в переменную типа sig_atomic_t (возможно, volatile -квалифицированный) и вернуться. Как правило, вы не можете вызывать большинство функций и не должны выполнять запись в глобальную память. Другими словами, обработчик должен просто установить флаг для проверки внутри вашей основной процедуры в какой-то момент, который вы сочтете подходящим, и оттуда должно выполняться действие, являющееся результатом самого сигнала.

(Поскольку может быть задействован блокирующий ввод-вывод, рассмотрите возможность изучения отмены потока POSIX Ваш клон Unix (особенно Linux) может иметь особенности в отношении этого и вышеперечисленного.)

Что касается деструкторов, здесь нет никакой магии. Они будут выполнены, если управление покинет заданную область действия любыми средствами, определенными в языке. Выход из области действия другими способами (например, longjmp() или даже exit()) не запускает деструкторы.

Относительно общих практик отключения в этой области существуют разные мнения.

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

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

Просто сказать нет. Когда вы захотите уйти, позвоните exit() (или даже _exit(), но следите за несбрасываемым вводом-выводом) и все. Медленно завершающиеся программы раздражают больше, чем программы с медленным запуском.

person alecov    schedule 29.12.2013
comment
Разве не безопасно raise передавать сигналы другим потокам? longjmp также разрешено внутри обработчиков сигналов и, возможно, throw тоже. - person Ben Voigt; 29.12.2013
comment
@BenVoigt: Конечно, именно поэтому я сказал в целом. Можно вызвать любую безопасную для сигналов функцию, но согласно POSIX.1 (в SUSv2 то же самое указано в записи документации для signal()), поведение не определено, если обработчик ссылается на статическую память, отличную от записи в sig_atomic_t (или errno). в более поздних редакциях). Это делает невозможным полезный вызов большинства функций, включая pthread_kill() и longjmp(), по очевидным причинам. Наконец, я не знаю о семантике throw внутри обработчика сигналов по отношению к многопоточным приложениям POSIX. - person alecov; 06.01.2014