Почему перед уничтожением потока нужно вызывать join() или detach()?

Я не понимаю, почему при разрушении std::thread он должен находиться в состоянии join() или detach().

Присоединение ожидает завершения потока, а отсоединение - нет. Кажется, есть какое-то среднее состояние, которое я не понимаю. Поскольку я понимаю, что join и detach дополняют друг друга: если я не вызываю join(), то по умолчанию используется detach().

Скажем так: допустим, вы пишете программу, которая создает поток, и только позже в жизни этого потока вы вызываете join(), поэтому до тех пор, пока вы не вызовете join, поток в основном работал так, как если бы он был отсоединен, нет?

Логически detach() должно быть поведением по умолчанию для потоков, потому что это определение потоков, которые выполняются параллельно независимо от других потоков.

Итак, когда объект потока разрушается, почему вызывается terminate()? Почему стандарт не может просто рассматривать нить как отсоединенную?

Я не понимаю смысла завершения программы, когда join() или detached() не вызывались до разрушения потока. Какова цель этого?

UPDATE:

Я недавно сталкивался с этим. Энтони Уильямс в своей книге "Параллельность в действии" заявляет: "Одно из предложений для С++ 17 было для класса joining_thread, который был бы похож на std::thread, за исключением того, что он автоматически включался бы в деструктор так же, как это делает scoped_thread. Это не получило консенсуса в комитете, поэтому он не был принят в стандарт (хотя он все еще на пути к С++ 20 как std::jthread)... "

Ответ 1

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

При использовании pthread-ов POSIX дочерние потоки должны быть присоединены после выхода, иначе они продолжают занимать системные ресурсы (например, запись таблицы процессов в ядре). Это делается через pthread_join(). Windows имеет несколько аналогичную проблему, если процесс содержит HANDLE для дочернего потока; хотя Windows не требует полного соединения, процесс должен по-прежнему вызывать CloseHandle(), чтобы освободить свою учетную запись в потоке.

Поскольку std::thread является кроссплатформенной абстракцией, она ограничена требованием POSIX, которое требует объединения.

Теоретически деструктор std::thread мог бы вызвать pthread_join() вместо того, чтобы выдавать исключение, но это (субъективно) может повысить риск тупика. В то время как правильно написанная программа знает, когда вставлять соединение в безопасное время.

Смотрите также:

Ответ 2

Вы запутались, потому что вы объединяете объект std::thread с потоком выполнения, на который он ссылается. Объект std::thread - это объект C++ (набор байтов в памяти), который действует как ссылка на поток выполнения. Когда вы вызываете std::thread::detach происходит следующее std::thread объект std::thread "отсоединяется" от потока выполнения - он больше не ссылается на (любой) поток выполнения, и поток выполнения продолжает работать независимо. Но объект std::thread все еще существует, пока не будет уничтожен.

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

Ответ 3

Возможно, вы захотите, чтобы поток полностью очищался после себя, когда он не оставлял следов. Это будет означать, что вы можете начать поток, а затем забыть об этом.

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

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

Ответ 4

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

  1. Если join было по умолчанию, такое необработанное состояние может привести к блокировке собственного потока при уничтожении.

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

Ответ 5

Когда дескриптор активного потока выходит из области видимости, у вас есть несколько вариантов:

  1. присоединиться
  2. отрывать
  3. убить тему
  4. убить программу

Каждый из этих вариантов ужасен. Неважно, что вы выберете, это будет удивительно, запутанно, а не то, что вы хотели в большинстве ситуаций.


Возможно, упомянутый вами поток присоединения уже существует в форме std::async, который дает вам std::future, который блокирует до тех пор, пока не будет создан созданный поток, что делает неявное соединение. Но много вопросов о том, почему

std::async(std::launch::async, f);
g();

не запускает f и g одновременно указывают, насколько это сбивает с толку. Лучший из известных мне подходов - определить, что это ошибка программирования, и программист исправит ее, поэтому assert будет наиболее подходящим. К сожалению, стандарт пошел вместе с std::terminate.


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