Что происходит с отделенным потоком при выходе из main()?

Предположим, я запускаю std::thread и затем detach() его, так что поток продолжает выполняться, даже если std::thread, который когда-то представлял его, выходит из области видимости.

Предположим далее, что в программе нет надежного протокола для присоединения к отсоединенному потоку1, поэтому отсоединенный поток продолжает выполняться после выхода из main().

Я не могу найти в стандарте (точнее, в проекте N3797 C++14) ничего, что описывало бы, что должно происходить, ни 1.10, ни 30.3 не содержат соответствующих формулировок.

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

Если выход из main() с запущенными отсоединенными потоками является неопределенным поведением, то любое использование std::thread::detach() является неопределенным поведением, если только главный поток никогда не выходит2.

Таким образом, выход из main() с запущенными отсоединенными потоками должен иметь определенные эффекты. Вопрос в том, где (в стандарте С++, не в POSIX, не в документации ОС, ...) эти эффекты определены.

2 Отсоединенный поток не может быть присоединен (в смысле std::thread::join()). Вы можете ждать результатов от отделившихся потоков (например, через будущее из std::packaged_task, или с помощью счетного семафора, или флага и переменной условия), но это не гарантирует, что поток закончил выполнение. Действительно, если не поместить сигнальную часть в деструктор первого автоматического объекта потока, то в общем случае будет существовать код (деструкторы), выполняющийся после сигнального кода. Если ОС планирует, что главный поток будет потреблять результат и выходить до того, как отделившийся поток закончит выполнение указанных деструкторов, то что произойдет?

Комментарии к вопросу (1)
Решение

Ответ на первоначальный вопрос "что происходит с отсоединенным потоком при выходе из main()" таков:

Он продолжает работать (потому что стандарт не говорит, что он остановлен), и это хорошо определено, пока он не касается ни (автоматических|thread_local) переменных других потоков, ни статических объектов.

По-видимому, это разрешено, чтобы позволить менеджерам потоков быть статическими объектами (заметка в [basic.start.term]/4 говорит об этом, спасибо @dyp за указатель).

Проблемы возникают после завершения уничтожения статических объектов, поскольку тогда выполнение переходит в режим, в котором может выполняться только код, разрешенный в обработчиках сигналов ([basic.start.term]/1, 1-е предложение). Из стандартной библиотеки C++ это только библиотека (*[support.runtime]/9, 2-е предложение*). В частности, это - в общем случае - *исключает* `condition_variable` (можно ли использовать ее в обработчике сигнала - зависит от реализации, поскольку она не является частью).

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

Ответ на второй вопрос "могут ли отсоединенные потоки быть снова объединены" таков:

Да, с помощью семейства функций *_at_thread_exit (notify_all_at_thread_exit(),std::promise::set_value_at_thread_exit()`, ...).

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

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

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

Комментарии (7)

Отсоединяем нитки

Согласно std::thread::detach:

Отделяет поток выполнения от объекта потока, позволяя продолжить выполнение независимо. Все выделенные ресурсы будут освобождены после выхода потока.

Из pthread_detach:

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

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

  1. Освободить хэндл на стороне приложения: Можно позволить объекту std::thread выйти из области видимости без присоединения, что обычно приводит к вызову std::terminate() при уничтожении.
  2. Позволить ОС автоматически очищать ресурсы, специфичные для потока (TCB), как только поток выходит, поскольку мы явно указали, что не заинтересованы в присоединении к потоку в дальнейшем, поэтому нельзя присоединиться к уже отсоединенному потоку.

Убийство потоков

Поведение при завершении процесса такое же, как и у основного потока, который, по крайней мере, может перехватывать некоторые сигналы. Могут ли другие потоки обрабатывать сигналы, не так важно, поскольку можно присоединить или завершить другие потоки в рамках вызова обработчика сигналов главного потока'. (Смежный вопрос)

Как уже говорилось, любой поток, отсоединенный или нет, в большинстве ОС умрет вместе со своим процессом. Сам процесс может быть завершен поднятием сигнала, вызовом exit() или возвратом из главной функции. Однако C++11 не может и не пытается определить точное поведение базовой ОС, в то время как разработчики Java VM, несомненно, могут в некоторой степени абстрагировать такие различия. AFAIK, экзотические модели процессов и потоков обычно встречаются на древних платформах (на которые C++11, вероятно, не будет перенесен) и различных встраиваемых системах, которые могут иметь специальную и/или ограниченную реализацию языковой библиотеки, а также ограниченную языковую поддержку.

Поддержка потоков

Если потоки не поддерживаются, то std::thread::get_id() должен возвращать неверный id (построенный по умолчанию std::thread::id), так как существует простой процесс, которому не нужен объект потока для запуска, а конструктор std::thread должен выбрасывать std::system_error. Именно так я понимаю C++11 в связке с современными ОС. Если есть ОС с поддержкой потоков, которая не порождает главный поток в своих процессах, дайте мне знать.

Управление потоками

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

Комментарии (2)

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

При отсоединении std::thread эти три условия продолжают выполняться:

  1. *this больше не владеет ни одним потоком
  2. joinable() всегда будет равно false
  3. get_id() будет равно std::thread::id().
Комментарии (4)

Рассмотрим следующий код:


#include 
#include 
#include 
#include 

void thread_fn() {
  std::this_thread::sleep_for (std::chrono::seconds(1)); 
  std::cout 
Комментарии (1)

Когда основной поток (то есть поток, который запускается функция main ()) завершается, а затем процесс прекращается, и все остальные потоки остановить.

Ссылка: https://stackoverflow.com/a/4667273/2194843

Комментарии (0)

Чтобы разрешить другим потокам продолжать выполнение основной поток должен завершить путем вызова pthread_exit (), а не выход(3). Это's прекрасно, чтобы использовать pthread_exit в главном. При использовании pthread_exit, основной поток закончит работу и останется в зомби(несуществующей) статус пока другого выхода нити. Если вы используете pthread_exit в основном потоке, не может вам вернуть состояние других потоков и не может сделать очистки для других потоков (может быть сделано с помощью pthread_join(3)). Кроме того, он's лучше, чтобы разделить потоки(pthread_detach(3)) так что-нить ресурсы освобождаются автоматически при прекращении потока. Общие ресурсы не будут освобождены, пока все выездные потоки.

Комментарии (2)