Co dzieje się z odłączonym wątkiem po zakończeniu działania funkcji main ()?


153

Załóżmy, że zaczynam std::threada detach(), więc wątek kontynuuje wykonywanie, mimo że ten, std::threadktóry kiedyś go reprezentował, wykracza poza zakres.

Załóżmy ponadto, że program nie ma niezawodnego protokołu do przyłączania się do odłączonego wątku 1 , więc odłączony wątek nadal działa po zakończeniu main().

Nie mogę znaleźć niczego w standardzie (a dokładniej w szkicu N3797 C ++ 14), który opisuje, co powinno się stać, ani 1.10, ani 30.3 nie zawierają stosownego sformułowania.

1 Kolejne, prawdopodobnie równoważne, pytanie brzmi: „czy odłączony wątek może kiedykolwiek zostać ponownie połączony”, ponieważ niezależnie od tego, jaki protokół zostanie wymyślony do przyłączenia, część sygnalizacyjna musiałaby zostać wykonana, gdy wątek nadal działał, a harmonogram systemu operacyjnego mógłby zdecydować o uśpieniu wątku na godzinę tuż po zakończeniu sygnalizacji, bez możliwości niezawodnego wykrycia przez odbiorcę, że wątek faktycznie się zakończył.

Jeśli wyczerpanie się main()z odłączonymi wątkami jest niezdefiniowanym zachowaniem, to każde użycie std::thread::detach()jest niezdefiniowane, chyba że główny wątek nigdy nie kończy pracy 2 .

Zatem wyczerpanie się main()z odłączonymi wątkami musi mieć określone efekty. Pytanie brzmi: gdzie (w standardzie C ++ , nie POSIX, nie dokumentacja systemu operacyjnego, ...) są te efekty zdefiniowane.

2 Oderwanej nitki nie można łączyć (w sensie std::thread::join()). Ty może czekać na wyniki z wolnostojących wątków (np poprzez przyszłość z std::packaged_tasklub przez semafor liczenia lub flagi i zmiennej stan), ale to nie gwarantuje, że wątek zakończeniu wykonywania . Rzeczywiście, chyba że można umieścić rolę sygnalizacyjną do destructor pierwszego automatycznego przedmiotu wątku, nie będzie w ogóle, być kod (destruktory), który prowadzony po kodu sygnałowego. Jeśli system operacyjny zaplanuje, że główny wątek zużyje wynik i zakończy działanie, zanim odłączony wątek zakończy działanie wspomnianych destruktorów, co ^ Wis zostanie zdefiniowane jako wystąpienie?


5
W [basic.start.term] / 4 mogę znaleźć tylko bardzo niejasną, nieobowiązkową notatkę: „Zakończenie każdego wątku przed wywołaniem std::exitlub wyjściem z mainjest wystarczające, ale nie jest konieczne, aby spełnić te wymagania”. (cały akapit może mieć znaczenie) Zobacz także [support.start.term] / 8 ( std::exitjest wywoływany, gdy mainzwraca)
dyp

Odpowiedzi:


45

Odpowiedź na pierwotne pytanie „co dzieje się z odłączonym wątkiem po zakończeniu działania main()” brzmi:

Nadal działa (ponieważ standard nie mówi, że jest zatrzymany) i jest to dobrze zdefiniowane, o ile nie dotyka ani (automatic | thread_local) zmiennych innych wątków ani obiektów statycznych.

Wydaje się, że jest to dozwolone, aby pozwolić menedżerom wątków na statyczne obiekty (uwaga w [basic.start.term] / 4 mówi to samo, dzięki @dyp za wskaźnik).

Problemy pojawiają się po zakończeniu niszczenia obiektów statycznych, ponieważ wtedy wykonanie wchodzi w stan, w którym może być wykonywany tylko kod dozwolony w programach obsługi sygnału ( [basic.start.term] / 1, pierwsze zdanie ). Ze standardowej biblioteki C ++ jest to tylko <atomic>biblioteka ( [support.runtime] / 9, drugie zdanie ). W szczególności to - ogólnie - wyklucza condition_variable (jest zdefiniowane w implementacji, czy jest to zapisywane do użycia w obsłudze sygnału, ponieważ nie jest częścią <atomic>).

Jeśli w tym momencie nie rozwinąłeś swojego stacka, trudno jest zrozumieć, jak uniknąć niezdefiniowanego zachowania.

Odpowiedź na drugie pytanie „czy odłączone wątki mogą być kiedykolwiek ponownie połączone” brzmi:

Tak, z *_at_thread_exitrodziny funkcji ( notify_all_at_thread_exit(), std::promise::set_value_at_thread_exit()...).

Jak zaznaczono w przypisie [2] pytania, sygnalizacja zmiennej warunkowej lub semafora lub licznika atomowego nie wystarczy do przyłączenia się do odłączonego wątku (w sensie zapewnienia, że ​​koniec jego wykonania nastąpił przed odebraniem wspomniana sygnalizacja przez oczekujący wątek), ponieważ generalnie będzie więcej kodu wykonanego np. po notify_all()zmiennej warunkowej, w szczególności destruktory obiektów automatycznych i lokalnych dla wątków.

Uruchomienie sygnalizacji jako ostatnią rzeczą wątek robi ( po destruktory automatycznych i nici lokalnego obiektów ma-stało ) jest to, co _at_thread_exitrodzina funkcji została zaprojektowana.

Tak więc, aby uniknąć niezdefiniowanego zachowania w przypadku braku jakichkolwiek gwarancji implementacji powyżej tego, czego wymaga standard, musisz (ręcznie) połączyć odłączony wątek z _at_thread_exitfunkcją wykonującą sygnalizację lub sprawić, aby odłączony wątek wykonywał tylko kod, który byłby bezpieczny dla obsługa sygnału też.


17
Jesteś tego pewien? Wszędzie, gdzie testowałem (GCC 5, clang 3.5, MSVC 14), wszystkie odłączone wątki są zabijane, gdy główny wątek kończy pracę.
rustyx

3
Uważam, że nie chodzi o to, co robi konkretna implementacja, ale o to, jak uniknąć tego, co standard definiuje jako niezdefiniowane zachowanie.
Jon Spencer

7
Ta odpowiedź wydaje się sugerować, że po zniszczeniu zmiennych statycznych proces przejdzie w stan uśpienia, czekając na zakończenie pozostałych wątków. To nie jest prawda, po exitzakończeniu niszczenia statycznych obiektów, uruchomionych atexitprocedur obsługi, opróżniania strumieni itp. Zwraca sterowanie do środowiska hosta, tj. Proces kończy się. Jeśli odłączony wątek nadal działa (i w jakiś sposób uniknął niezdefiniowanego zachowania, nie dotykając niczego poza własnym wątkiem), po prostu znika w obłoku dymu, gdy proces kończy się.
Jonathan Wakely

3
Jeśli wszystko jest w porządku, używając interfejsów API C ++ innych niż ISO, to jeśli mainwywołania pthread_exitzamiast zwracać lub wywoływać exit, spowoduje to, że proces będzie czekał na zakończenie odłączonych wątków, a następnie wywoła exitpo zakończeniu ostatniego.
Jonathan Wakely

3
„Kontynuuje działanie (ponieważ standard nie mówi, że jest zatrzymany)” -> Czy ktoś może mi powiedzieć, JAK wątek może kontynuować wykonywanie bez przetwarzania kontenera?
Gupta

42

Odłączanie wątków

Według std::thread::detach:

Oddziela wątek wykonania od obiektu wątku, umożliwiając niezależne kontynuowanie wykonywania. Wszelkie przydzielone zasoby zostaną zwolnione po zamknięciu wątku.

Od pthread_detach:

Funkcja pthread_detach () powinna wskazywać implementacji, że pamięć dla wątku może zostać odzyskana po zakończeniu tego wątku. Jeśli wątek nie został zakończony, pthread_detach () nie spowoduje zakończenia. Efekt wielu wywołań pthread_detach () w tym samym wątku docelowym jest nieokreślony.

Odłączanie wątków służy głównie do oszczędzania zasobów, w przypadku gdy aplikacja nie musi czekać na zakończenie wątku (np. Demony, które muszą działać do zakończenia procesu):

  1. Aby zwolnić uchwyt boczny aplikacji: Można pozwolić std::threadobiektowi wyjść poza zakres bez łączenia, co zwykle prowadzi do wezwania do std::terminate()zniszczenia.
  2. Aby umożliwić systemowi operacyjnemu automatyczne czyszczenie zasobów specyficznych dla wątku ( TCB ), gdy tylko wątek zostanie zamknięty, ponieważ wyraźnie określiliśmy, że nie jesteśmy zainteresowani późniejszym dołączaniem do wątku, w związku z tym nie można dołączyć do już odłączonego wątku.

Zabijanie wątków

Zachowanie po zakończeniu procesu jest takie samo, jak w przypadku wątku głównego, który może przynajmniej przechwycić niektóre sygnały. To, czy inne wątki mogą obsługiwać sygnały, nie jest tak ważne, ponieważ można łączyć lub kończyć inne wątki w ramach wywołania funkcji obsługi sygnału głównego wątku. (Powiązane pytanie )

Jak już wspomniano, każdy wątek, odłączony lub nie, umrze wraz z procesem w większości systemów operacyjnych . Sam proces można zakończyć przez podniesienie sygnału, wywołanie exit()lub powrót z funkcji głównej. Jednak C ++ 11 nie może i nie próbuje zdefiniować dokładnego zachowania bazowego systemu operacyjnego, podczas gdy programiści maszyny wirtualnej Java z pewnością mogą do pewnego stopnia wyabstrahować takie różnice. AFAIK, egzotyczne modele procesów i wątków są zwykle spotykane na starożytnych platformach (do których C ++ 11 prawdopodobnie nie zostanie przeniesiony) i różnych systemach wbudowanych, które mogą mieć specjalną i / lub ograniczoną implementację biblioteki językowej, a także ograniczoną obsługę języków.

Obsługa wątków

Jeśli wątki nie są obsługiwane, std::thread::get_id()powinny zwrócić nieprawidłowy identyfikator (domyślnie skonstruowany std::thread::id), ponieważ istnieje zwykły proces, który nie potrzebuje obiektu wątku do uruchomienia, a konstruktor a std::threadpowinien wyrzucić plik std::system_error. Oto jak rozumiem C ++ 11 w połączeniu z dzisiejszymi systemami operacyjnymi. Jeśli istnieje system operacyjny z obsługą wątków, który nie tworzy głównego wątku w swoich procesach, daj mi znać.

Kontrolowanie wątków

Jeśli trzeba zachować kontrolę nad wątkiem w celu prawidłowego zamknięcia, można to zrobić za pomocą prymitywów synchronizacji i / lub jakiegoś rodzaju flag. Jednak w tym przypadku ustawienie flagi zamknięcia, po której następuje łączenie, jest sposobem, który preferuję, ponieważ nie ma sensu zwiększać złożoności przez odłączanie wątków, ponieważ i tak zasoby zostałyby zwolnione w tym samym czasie, gdzie kilka bajtów std::threadobiektu w porównaniu z wyższą złożonością i prawdopodobnie bardziej prymitywami synchronizacji powinny być dopuszczalne.


3
Ponieważ każdy wątek ma swój własny stos (który jest w zakresie megabajtów w systemie Linux), wybrałbym odłączenie wątku (więc jego stos zostanie zwolniony, gdy tylko zostanie zamknięty) i użyję kilku prymitywów synchronizacji, jeśli główny wątek musi zostać zamknięty (i dla prawidłowego zamknięcia musi dołączyć do wciąż działających wątków zamiast kończyć je po powrocie / wyjściu).
Norbert Bérci

8
Naprawdę nie rozumiem, jak to odpowiada na pytanie
MikeMB

18

Rozważ następujący kod:

#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; 
}

Uruchamiając go w systemie Linux, wiadomość z thread_fn nigdy nie jest drukowana. System operacyjny rzeczywiście czyści się thread_fn()zaraz po main()zamknięciu. Wymiana t1.detach()z t1.join()zawsze drukuje wiadomość, jak oczekiwano.


To zachowanie występuje dokładnie w systemie Windows. Wygląda więc na to, że Windows zabija odłączone wątki po zakończeniu programu.
Gupta

17

Los wątku po zakończeniu programu jest niezdefiniowanym zachowaniem. Ale nowoczesny system operacyjny czyści wszystkie wątki utworzone przez proces po jego zamknięciu.

Podczas odłączania std::threadte trzy warunki będą nadal obowiązywać:

  1. *this nie posiada już żadnego wątku
  2. joinable() zawsze będzie równa false
  3. get_id() będzie równa std::thread::id()

1
Dlaczego niezdefiniowany? Bo norma niczego nie definiuje? Swoją drogą, czy nie oznaczałoby to żadnego wezwania do detach()nieokreślonego zachowania? Trudno w to uwierzyć ...
Marc Mutz - mmutz

2
@ MarcMutz-mmutz Jest nieokreślony w tym sensie, że jeśli proces zakończy się, los wątku jest nieokreślony.
Caesar

2
@Caesar i jak upewnić się, że nie zamykam przed zakończeniem wątku?
MichalH


0

Aby umożliwić innym wątkom kontynuowanie wykonywania, główny wątek powinien zakończyć się przez wywołanie pthread_exit () zamiast exit (3). Dobrze jest użyć pthread_exit w main. Kiedy używany jest pthread_exit, główny wątek przestanie działać i pozostanie w stanie zombie (niedziałający), dopóki wszystkie inne wątki nie zostaną zakończone. Jeśli używasz pthread_exit w głównym wątku, nie możesz uzyskać statusu zwrotu innych wątków i nie możesz wyczyścić innych wątków (można to zrobić za pomocą pthread_join (3)). Ponadto lepiej jest odłączać wątki (pthread_detach (3)), aby zasoby wątków były automatycznie zwalniane po zakończeniu wątku. Udostępnione zasoby nie zostaną zwolnione, dopóki wszystkie wątki nie zostaną zakończone.


@kgvinod, dlaczego nie dodać „pthread_exit (0);” po „ti.detach ()”;
yshi

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.