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

153

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

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

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

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

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

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

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

Марк Муц - ммц
источник
5
Я могу найти только очень расплывчатую необязательную заметку в [basic.start.term] / 4: «Завершение каждого потока перед вызовом std::exitили выходом из mainнего достаточно, но не обязательно, чтобы удовлетворить эти требования». (весь абзац может быть релевантным) Также см. [support.start.term] / 8 ( std::exitвызывается, когда mainвозвращается)
dyp

Ответы:

45

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

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

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

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

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

Ответ на второй вопрос «могут ли отдельные потоки когда-либо присоединяться снова»:

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

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

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

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

Марк Муц - ммц
источник
17
вы уверены в этом? Везде, где я тестировал (GCC 5, clang 3.5, MSVC 14), все отключенные потоки уничтожаются при выходе из основного потока.
rustyx
3
Я считаю, что проблема не в том, что делает конкретная реализация, а в том, как избежать того, что стандарт определяет как неопределенное поведение.
Джон Спенсер
7
Этот ответ, по-видимому, подразумевает, что после уничтожения статических переменных процесс перейдет в некое состояние ожидания, ожидая завершения всех оставшихся потоков. Это не так, после exitзавершения уничтожения статических объектов, запуска atexitобработчиков, очистки потоков и т. Д. Он возвращает управление среде хоста, то есть процесс завершается. Если отсоединенный поток все еще работает (и каким-то образом избежал неопределенного поведения, не касаясь чего-либо за пределами своего собственного потока), он просто исчезает в клубе дыма при выходе из процесса.
Джонатан Уэйкли
3
Если вы в порядке, используя API-интерфейсы C ++, отличные от ISO, то если mainвызовы pthread_exitвместо возврата или вызова exit, это заставит процесс ждать завершения отдельных потоков, а затем вызовет exitпосле завершения последнего из них.
Джонатан Уэйкли
3
«Он продолжает работать (потому что стандарт не говорит, что он остановлен)» -> Кто-нибудь может сказать мне, КАК поток может продолжать выполнение во время процесса контейнера?
Гупта
42

Отсоединение темы

По словам 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()должны возвращать недопустимый идентификатор (по умолчанию std::thread::idсоздан), поскольку существует простой процесс, которому не нужен объект потока для выполнения, и конструктор объекта std::threadдолжен выдать a std::system_error. Вот как я понимаю C ++ 11 в сочетании с современными ОС. Если есть ОС с поддержкой потоков, которая не порождает основной поток в своих процессах, дайте мне знать.

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

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

Sam
источник
3
Поскольку каждый поток имеет свой собственный стек (который находится в диапазоне мегабайт в Linux), я бы предпочел отсоединить поток (чтобы его стек освободился, как только он выйдет) и использовать некоторые примитивы синхронизации, если основной поток должен выйти (и для правильного завершения он должен присоединиться к все еще работающим потокам, а не завершать их при возврате / выходе).
Норберт Берси
8
Я действительно не понимаю, как это отвечает на вопрос
MikeMB
18

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

#include <iostream>
#include <string>
#include <thread>
#include <chrono>

void thread_fn() {
  std::this_thread::sleep_for (std::chrono::seconds(1)); 
  std::cout << "Inside thread function\n";   
}

int main()
{
    std::thread t1(thread_fn);
    t1.detach();

    return 0; 
}

При запуске в системе Linux сообщение из thread_fn никогда не печатается. ОС действительно очищается, thread_fn()как только main()выходит. Замена t1.detach()с t1.join()всегда печатает сообщение , как ожидалось.

kgvinod
источник
Такое поведение происходит именно в Windows. Таким образом, кажется, что Windows убивает отдельные потоки, когда программа завершена.
Гупта
17

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

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

  1. *this больше не владеет какой-нить
  2. joinable() всегда будет равно false
  3. get_id() будет равно std::thread::id()
Цезарь
источник
1
Почему не определено? Потому что стандарт ничего не определяет? Судя по моей сноске, разве это не вызовет detach()неопределенного поведения? Трудно поверить ...
Марк Мутц - mmutz
2
@ MarcMutz-mmutz Не определено в том смысле, что если процесс завершается, судьба потока не определена.
Цезарь
2
@Caesar, и как мне убедиться, что выход не завершен до завершения потока?
MichalH
6

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

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

испуг
источник
0

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

yshi
источник
@kgvinod, почему бы не добавить "pthread_exit (0);" после "ti.detach ()";
Yshi