Що відбувається з відокремленою ниткою при виході main ()?


153

Припустимо, я починаю a, std::threadа потім detach()це, тому нитка продовжує виконувати, навіть незважаючи на 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, або за допомогою підрахунку семафору, прапора та змінної умови), але це не гарантує, що потік завершено виконувати . У самому справі, якщо не поставити сигналізацію частини в деструктор першого автоматичного об'єкта потоку, там буде , взагалі кажучи , код (деструктори) , які виконуються після коду сигналізації. Якщо ОС планує основний потік, щоб споживати результат і виходити до того, як відокремлений потік закінчить роботу зазначених деструкторів, що буде ^ WS визначено?


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

Відповіді:


45

Відповідь на початкове запитання "що відбувається з відірваною ниткою при main()виході":

Він продовжує працювати (оскільки стандарт не каже, що він зупинений), і це чітко визначено, доки він не торкається жодних (автоматичних | ниток_локальних) змінних інших потоків, а також статичних об'єктів.

Здається, це дозволяє дозволити менеджерам потоків статичним об’єктам (зауважте в [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, клакс 3.5, MSVC 14), усі відокремлені нитки вбиваються при виході основної нитки.
rustyx

3
Я вважаю, що питання полягає не в тому, що робить конкретна реалізація, а в тому, як уникнути того, що стандарт визначає як невизначену поведінку.
Джон Спенсер

7
Здається, ця відповідь означає, що після руйнування статичних змінних процес перейде в стан сплячого режиму, очікуючи закінчення будь-яких інших потоків. Це неправда, після exitзавершення руйнування статичних об'єктів, запущених atexitобробників, промивних потоків тощо він повертає контроль до хост-середовища, тобто процес виходить. Якщо відокремлена нитка все ще працює (і якось уникала невизначеної поведінки, не торкаючись нічого поза власною ниткою), вона просто зникає в задуху диму, коли процес закінчується.
Джонатан Уейклі

3
Якщо ви все в порядку, використовуючи API, які не відповідають стандартам ISO C ++, тоді, якщо 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), оскільки там є звичайний процес, якому не потрібен об’єкт потоку для запуску, і конструктор a std::threadповинен кинутиstd::system_error . Ось як я розумію C ++ 11 спільно з сьогоднішніми ОС. Якщо є ОС з підтримкою нитки, яка не породить основну нитку в її процесах, дайте мені знати.

Керування нитками

Якщо вам потрібно зберегти контроль над потоком для належного відключення, це можна зробити за допомогою примітивів синхронізації та / або якихось прапорів. Однак у цьому випадку я вважаю за краще встановити прапор відключення з подальшим об'єднанням, оскільки немає сенсу збільшувати складність, від'єднуючи нитки, оскільки ресурси будуть вивільнені одночасно, де мало байтів std::threadоб'єкта порівняно з більш високою складністю та, можливо, з більшою кількістю примітивів для синхронізації.


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()завжди друкує повідомлення, як очікувалося.


Така поведінка відбувається саме в 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


0

Щоб дозволити іншим потокам продовжувати виконання, основний потік повинен закінчуватися викликом pthread_exit (), а не виходу (3). Добре використовувати pthread_exit в основному. Коли використовується pthread_exit, головний потік припинить виконання і залишатиметься у статусі зомбі (неіснуючого) до тих пір, поки всі інші потоки не вийдуть. Якщо ви використовуєте pthread_exit в основному потоці, не можна отримати статус повернення інших потоків і не зможете очистити інші потоки (це можна зробити за допомогою pthread_join (3)). Крім того, краще від'єднати потоки (pthread_detach (3)), щоб ресурси потоку автоматично звільнялися після завершення потоку. Спільні ресурси не будуть випущені, поки не вийдуть всі потоки.


@kgvinod, чому б не додати "pthread_exit (0);" після "ti.detach ()";
yshi

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.