Apa yang terjadi pada utas terpisah ketika main () keluar?

152

Asumsikan saya memulai std::threaddan kemudian detach(), jadi utas terus mengeksekusi meskipun std::threadyang pernah mewakilinya, keluar dari ruang lingkup.

Asumsikan lebih lanjut bahwa program tidak memiliki protokol yang dapat diandalkan untuk bergabung dengan utas yang dilepaskan 1 , sehingga utas yang terlepas masih berjalan saat main()keluar.

Saya tidak dapat menemukan apa pun dalam standar (lebih tepatnya, dalam N3797 C ++ 14 draft), yang menggambarkan apa yang harus terjadi, baik 1,10 maupun 30,3 berisi kata-kata yang bersangkutan.

1 Pertanyaan lain, yang mungkin setara, adalah: "dapatkah utas terpisah dilepas lagi", karena protokol apa pun yang ingin Anda gabungkan, bagian pensinyalan harus dilakukan saat utas masih berjalan, dan penjadwal OS mungkin memutuskan untuk meletakkan utas untuk tidur selama satu jam tepat setelah pensinyalan dilakukan dengan tidak ada cara bagi pihak penerima untuk secara andal mendeteksi bahwa utas benar-benar selesai.

Jika kehabisan main()dengan menjalankan thread terpisah menjalankan perilaku tidak terdefinisi, maka setiap penggunaan std::thread::detach()perilaku tidak terdefinisi kecuali utas utama tidak pernah keluar 2 .

Dengan demikian, kehabisan main()dengan thread terpisah berjalan harus memiliki efek yang ditentukan . Pertanyaannya adalah: di mana (dalam standar C ++ , bukan POSIX, bukan OS docs, ...) adalah efek yang didefinisikan.

2 Utas yang terlepas tidak dapat bergabung (dalam arti std::thread::join()). Anda bisa menunggu hasil dari utas terpisah (mis. Melalui masa depan dari std::packaged_task, atau dengan menghitung semaphore atau bendera dan variabel kondisi), tetapi itu tidak menjamin bahwa utas telah selesai dieksekusi . Memang, kecuali Anda meletakkan bagian pensinyalan ke dalam destruktor dari objek otomatis utas pertama, akan ada , secara umum, menjadi kode (destruktor) yang dijalankan setelah kode pensinyalan. Jika OS menjadwalkan utas utama untuk mengonsumsi hasil dan keluar sebelum utas terpisah selesai menjalankan kata destructor, apa yang akan didefinisikan untuk terjadi?

Marc Mutz - mmutz
sumber
5
Saya hanya dapat menemukan catatan non-wajib yang sangat samar di [basic.start.term] / 4: "Mengakhiri setiap utas sebelum panggilan ke std::exitatau keluar dari maincukup, tetapi tidak perlu, untuk memenuhi persyaratan ini." (seluruh paragraf mungkin relevan) Lihat juga [support.start.term] / 8 ( std::exitdipanggil saat mainkembali)
dyp

Jawaban:

45

Jawaban untuk pertanyaan awal "apa yang terjadi pada utas terlepas ketika main()keluar" adalah:

Ini terus berjalan (karena standar tidak mengatakan itu dihentikan), dan itu didefinisikan dengan baik, selama tidak menyentuh variabel (otomatis | thread_local) dari thread lain atau objek statis.

Ini tampaknya diizinkan untuk mengizinkan manajer thread sebagai objek statis (catatan di [basic.start.term] / 4 mengatakan sebanyak itu, terima kasih kepada @dyp untuk penunjuknya).

Masalah muncul ketika penghancuran objek statis telah selesai, karena kemudian eksekusi memasuki rezim di mana hanya kode yang diizinkan dalam penangan sinyal yang dapat dieksekusi ( [basic.start.term] / 1, kalimat pertama ). Dari pustaka standar C ++, itu hanya <atomic>pustaka ( [support.runtime] / 9, kalimat ke-2 ). Secara khusus, itu — secara umum — tidak termasuk condition_variable (itu ditentukan implementasi apakah itu disimpan untuk digunakan dalam penangan sinyal, karena itu bukan bagian dari<atomic> ).

Kecuali jika Anda telah melepaskan tumpukan Anda pada saat ini, sulit untuk melihat bagaimana menghindari perilaku yang tidak terdefinisi.

Jawaban untuk pertanyaan kedua "dapat memisahkan utas yang pernah bergabung lagi" adalah:

Ya, dengan *_at_thread_exitkeluarga fungsi ( notify_all_at_thread_exit(), std::promise::set_value_at_thread_exit(), ...).

Seperti dicatat dalam catatan kaki [2] dari pertanyaan, memberi sinyal variabel kondisi atau semafor atau penghitung atom tidak cukup untuk bergabung dengan utas terpisah (dalam arti memastikan bahwa akhir pelaksanaannya) telah terjadi-sebelum menerima kata signaling oleh thread menunggu), karena, secara umum, akan ada lebih banyak kode yang dieksekusi setelah misalnya notify_all()dari variabel kondisi, khususnya destruktor objek otomatis dan thread-lokal.

Menjalankan pensinyalan sebagai hal terakhir yang dilakukan thread ( setelah penghancur objek otomatis dan thread-lokal telah terjadi ) adalah tujuan _at_thread_exitdari rangkaian fungsi.

Jadi, untuk menghindari perilaku tidak terdefinisi dengan tidak adanya jaminan implementasi di atas apa yang disyaratkan standar, Anda perlu (secara manual) bergabung dengan utas terpisah dengan _at_thread_exitfungsi melakukan pensinyalan atau membuat utas terpisah melepaskan hanya kode yang akan aman untuk penangan sinyal juga.

Marc Mutz - mmutz
sumber
17
Apa kau yakin tentang ini? Di mana-mana saya menguji (GCC 5, dentang 3.5, MSVC 14), semua utas terlepas terbunuh saat utas utama keluar.
rustyx
3
Saya percaya bahwa masalahnya bukan apa yang dilakukan oleh implementasi spesifik, tetapi bagaimana menghindari standar yang didefinisikan sebagai perilaku yang tidak terdefinisi.
Jon Spencer
7
Jawaban ini tampaknya menyiratkan bahwa setelah penghancuran variabel statis proses akan masuk ke semacam keadaan tidur menunggu utas yang tersisa untuk menyelesaikan. Itu tidak benar, setelah exitselesai menghancurkan objek statis, menjalankan atexitpenangan, pembilasan aliran dll. Ia mengembalikan kontrol ke lingkungan host, yaitu proses keluar. Jika utas terpisah masih berjalan (dan entah bagaimana menghindari perilaku yang tidak terdefinisi dengan tidak menyentuh apa pun di luar utasnya sendiri), maka itu hanya menghilang dalam kepulan asap saat proses keluar.
Jonathan Wakely
3
Jika Anda baik-baik saja menggunakan API non-ISO C ++ lalu jika mainpanggilan pthread_exitalih-alih kembali atau menelepon exitmaka itu akan menyebabkan proses untuk menunggu utas terlepas selesai, dan kemudian menelepon exitsetelah yang terakhir selesai.
Jonathan Wakely
3
"Ini terus berjalan (karena standar tidak mengatakan itu dihentikan)" -> Adakah yang bisa memberitahu saya BAGAIMANA sebuah thread dapat melanjutkan eksekusi ketika proses wadahnya?
Gupta
42

Melepaskan Utas

Menurut std::thread::detach:

Memisahkan utas eksekusi dari objek utas, memungkinkan eksekusi berlanjut secara mandiri. Setiap sumber daya yang dialokasikan akan dibebaskan setelah utas keluar.

Dari pthread_detach:

Fungsi pthread_detach () harus menunjukkan pada implementasi bahwa penyimpanan untuk utas dapat direklamasi ketika utas itu berakhir. Jika utas belum berakhir, pthread_detach () tidak akan membuatnya berhenti. Efek dari beberapa panggilan pthread_detach () pada utas target yang sama tidak ditentukan.

Melepaskan utas terutama untuk menghemat sumber daya, jika aplikasi tidak perlu menunggu utas selesai (mis. Daemon, yang harus dijalankan hingga proses penghentian):

  1. Untuk membebaskan pegangan sisi aplikasi: Seseorang dapat membiarkan std::threadobjek keluar dari ruang lingkup tanpa bergabung, yang biasanya mengarah pada panggilan untuk std::terminate()dihancurkan.
  2. Untuk memungkinkan OS membersihkan sumber daya spesifik utas ( TCB ) secara otomatis segera setelah utas keluar, karena kami secara eksplisit menentukan, bahwa kami tidak tertarik untuk bergabung dengan utas nanti, dengan demikian, seseorang tidak dapat bergabung dengan utas yang sudah terlepas.

Thread Pembunuh

Perilaku penghentian proses sama dengan perilaku untuk utas utama, yang setidaknya bisa menangkap beberapa sinyal. Apakah thread lain dapat menangani sinyal tidak penting, karena seseorang dapat bergabung atau mengakhiri thread lain dalam doa penangan sinyal utama. (Pertanyaan terkait )

Seperti yang telah dinyatakan, utas apa pun, terlepas atau tidak, akan mati dengan prosesnya di sebagian besar OS . Proses itu sendiri dapat diakhiri dengan menaikkan sinyal, dengan meneleponexit() atau dengan kembali dari fungsi utama. Namun, C ++ 11 tidak dapat dan tidak mencoba untuk mendefinisikan perilaku yang tepat dari OS yang mendasarinya, sedangkan pengembang Java VM pasti dapat mengabstraksikan perbedaan tersebut sampai batas tertentu. AFAIK, proses eksotis dan model threading biasanya ditemukan pada platform kuno (yang C ++ 11 mungkin tidak akan porting) dan berbagai sistem embedded, yang dapat memiliki implementasi perpustakaan bahasa khusus dan / atau terbatas serta dukungan bahasa yang terbatas.

Dukungan Thread

Jika utas tidak didukung std::thread::get_id()harus mengembalikan id yang tidak valid (dibangun secara default std::thread::id) karena ada proses sederhana, yang tidak memerlukan objek utas untuk dijalankan dan konstruktor a std::threadharus melempar a std::system_error. Ini adalah bagaimana saya memahami C ++ 11 dalam hubungannya dengan OS hari ini. Jika ada OS dengan dukungan threading, yang tidak menelurkan utas utama dalam prosesnya, beri tahu saya.

Mengontrol Utas

Jika seseorang perlu menjaga kontrol atas utas untuk shutdown yang benar, seseorang dapat melakukannya dengan menggunakan sinkronisasi primitif dan / atau semacam bendera. Namun, dalam hal ini, pengaturan flag shutdown yang diikuti oleh join adalah cara yang saya sukai, karena tidak ada gunanya meningkatkan kompleksitas dengan melepaskan thread, karena sumber daya akan dibebaskan pada saat yang sama pula, di mana beberapa byte std::threadobjek vs. kompleksitas yang lebih tinggi dan kemungkinan primitif yang lebih sinkron harus dapat diterima.

Sam
sumber
3
Karena setiap utas memiliki tumpukan sendiri (yang berada dalam kisaran megabyte di Linux), saya akan memilih untuk melepaskan utas (sehingga tumpukannya akan dibebaskan segera setelah keluar) dan menggunakan beberapa primitif sinkronisasi jika utas utama perlu keluar (dan untuk shutdown yang benar, ia harus bergabung dengan utas yang masih berjalan alih-alih menghentikannya saat kembali / keluar).
Norbert Bérci
8
Saya benar-benar tidak melihat bagaimana ini menjawab pertanyaan
MikeMB
18

Pertimbangkan kode berikut:

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

Menjalankannya pada sistem Linux, pesan dari thread_fn tidak pernah dicetak. OS memang membersihkan thread_fn()segera setelah main()keluar. Mengganti t1.detach()dengan t1.join()selalu mencetak pesan seperti yang diharapkan.

kgvinod
sumber
Perilaku ini terjadi persis di Windows. Jadi, tampaknya Windows membunuh utas yang terlepas ketika program selesai.
Gupta
17

Nasib utas setelah program keluar adalah perilaku yang tidak ditentukan. Tetapi sistem operasi modern akan membersihkan semua utas yang dibuat oleh proses penutupannya.

Saat melepaskan sebuah std::thread, tiga kondisi ini akan terus bertahan:

  1. *this tidak lagi memiliki utas
  2. joinable() akan selalu sama dengan false
  3. get_id() akan sama std::thread::id()
Caesar
sumber
1
Kenapa tidak terdefinisi? Karena standar tidak mendefinisikan apa pun? Dengan catatan kaki saya, bukankah itu membuat panggilan untuk detach()memiliki perilaku yang tidak terdefinisi? Sulit dipercaya ...
Marc Mutz - mmutz
2
@ MarcMutz-mmutz Tidak terdefinisi dalam arti bahwa jika proses keluar, nasib utas tidak terdefinisi.
Caesar
2
@Caesar dan bagaimana saya memastikan tidak keluar sebelum utas selesai?
MichalH
0

Untuk mengizinkan utas lain melanjutkan eksekusi, utas utama harus diakhiri dengan memanggil pthread_exit () daripada keluar (3). Tidak apa-apa menggunakan pthread_exit di main. Ketika pthread_exit digunakan, utas utama akan berhenti mengeksekusi dan akan tetap dalam status zombie (mati) sampai semua utas lainnya keluar. Jika Anda menggunakan pthread_exit di utas utama, tidak bisa mendapatkan status pengembalian utas lainnya dan tidak dapat melakukan pembersihan untuk utas lainnya (bisa dilakukan menggunakan pthread_join (3)). Juga, lebih baik lepaskan utas (pthread_detach (3)) sehingga sumber utas dilepaskan secara otomatis pada penghentian utas. Sumber daya bersama tidak akan dirilis sampai semua utas keluar.

yshi
sumber
@ kgvinod, mengapa tidak menambahkan "pthread_exit (0);" setelah "ti.detach ()";
yshi