Saya menulis program multithreading sederhana sebagai berikut:
static bool finished = false;
int func()
{
size_t i = 0;
while (!finished)
++i;
return i;
}
int main()
{
auto result=std::async(std::launch::async, func);
std::this_thread::sleep_for(std::chrono::seconds(1));
finished=true;
std::cout<<"result ="<<result.get();
std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}
Ini berperilaku normal dalam mode debug di Visual studio atau -O0
di gc c dan mencetak hasilnya setelah beberapa 1
detik. Tetapi macet dan tidak mencetak apa pun dalam mode Rilis atau -O1 -O2 -O3
.
c++
multithreading
thread-safety
data-race
sz ppeter
sumber
sumber
Jawaban:
Dua utas, mengakses variabel non-atomik, non-dijaga adalah UB Kekhawatiran ini
finished
. Anda dapat membuatfinished
tipestd::atomic<bool>
untuk memperbaikinya.Perbaikan saya:
Keluaran:
Demo langsung di coliru
Seseorang mungkin berpikir 'Ini
bool
- mungkin sedikit. Bagaimana ini bisa non-atom? ' (Saya lakukan ketika saya mulai dengan multi-threading sendiri.)Tetapi perhatikan bahwa kekurangan air mata bukanlah satu-satunya hal yang terjadi
std::atomic
memberi Anda. Itu juga membuat akses baca + tulis bersamaan dari banyak utas didefinisikan dengan baik, menghentikan kompiler dari asumsi bahwa membaca ulang variabel akan selalu melihat nilai yang sama.Membuat
bool
non-atomic yang tidak dijaga dapat menyebabkan masalah tambahan:atomic<bool>
denganmemory_order_relaxed
store / load akan bekerja, tetapi di manavolatile
tidak. Menggunakan volatile untuk ini adalah UB, meskipun itu bekerja dalam praktik pada implementasi C ++ nyata.)Untuk mencegah hal ini terjadi, kompiler harus diberitahu secara eksplisit untuk tidak melakukannya.
Saya sedikit terkejut tentang diskusi yang berkembang tentang potensi hubungan
volatile
dengan masalah ini. Jadi, saya ingin menghabiskan dua sen:sumber
func()
dan berpikir "Saya bisa mengoptimalkannya begitu saja" Pengoptimal tidak peduli untuk thread sama sekali, dan akan mendeteksi loop tak terbatas, dan dengan senang hati akan mengubahnya menjadi "sementara (Benar)" Jika kita melihat godbolt .org / z / Tl44iN kita bisa melihat ini. Jika sudah selesaiTrue
ia kembali. Jika tidak, itu akan menjadi lompatan tanpa syarat kembali ke dirinya sendiri (sebuah loop tak terbatas) pada label.L5
volatile
di C ++ 11 karena Anda bisa mendapatkan asm identik denganatomic<T>
danstd::memory_order_relaxed
. Itu bekerja meskipun pada perangkat keras nyata: cache adalah koheren sehingga instruksi beban tidak bisa terus membaca nilai basi begitu toko pada inti lain berkomitmen untuk melakukan cache di sana. (MESI)volatile
masih UB sekalipun. Anda benar-benar tidak boleh berasumsi sesuatu yang pasti dan jelas UB aman hanya karena Anda tidak bisa memikirkan cara itu bisa salah dan berhasil ketika Anda mencobanya. Itu telah membuat orang terbakar berulang kali.finished
denganstd::mutex
karya (tanpavolatile
atauatomic
). Faktanya, Anda dapat mengganti semua atomik dengan skema nilai + mutex "sederhana"; masih akan bekerja dan hanya lebih lambat.atomic<T>
diizinkan menggunakan mutex internal; hanyaatomic_flag
dijamin bebas kunci.Jawaban Scheff menjelaskan cara memperbaiki kode Anda. Saya pikir saya akan menambahkan sedikit informasi tentang apa yang sebenarnya terjadi dalam kasus ini.
Saya mengkompilasi kode Anda di godbolt menggunakan optimasi level 1 (
-O1
). Fungsi Anda mengkompilasi seperti:Jadi, apa yang terjadi di sini? Pertama, kami memiliki perbandingan:
cmp BYTE PTR finished[rip], 0
- ini memeriksa untuk melihat apakahfinished
salah atau tidak.Jika tidak salah (alias benar) kita harus keluar dari loop pada proses pertama. Hal ini dicapai dengan
jne .L4
yang j umps ketika n ot e qual ke label.L4
dimana nilaii
(0
) disimpan dalam register untuk digunakan dan fungsi kembali.Jika adalah palsu namun, kami pindah ke
Ini adalah lompatan tanpa syarat, untuk memberi label
.L5
yang kebetulan merupakan perintah lompatan itu sendiri.Dengan kata lain, utas dimasukkan ke dalam loop sibuk tanpa batas.
Jadi mengapa ini terjadi?
Sejauh menyangkut optimiser, utas berada di luar ruang lingkupnya. Ini mengasumsikan utas lainnya tidak membaca atau menulis variabel secara bersamaan (karena itu akan menjadi data-ras UB). Anda perlu mengatakan bahwa itu tidak dapat mengoptimalkan akses jauh. Di sinilah jawaban Scheff masuk. Saya tidak akan repot-repot mengulanginya.
Karena pengoptimal tidak diberitahu bahwa
finished
variabel berpotensi berubah selama eksekusi fungsi, ia melihat bahwafinished
itu tidak dimodifikasi oleh fungsi itu sendiri dan mengasumsikan bahwa itu konstan.Kode yang dioptimalkan menyediakan dua jalur kode yang akan dihasilkan dari memasukkan fungsi dengan nilai bool konstan; baik itu menjalankan loop secara tak terbatas, atau loop tidak pernah berjalan.
di
-O0
compiler (seperti yang diharapkan) tidak mengoptimalkan loop body dan perbandingannya:oleh karena itu fungsinya, ketika tidak dioptimalkan berhasil, kurangnya atomisitas di sini biasanya tidak menjadi masalah, karena kode dan tipe data sederhana. Mungkin yang terburuk kita bisa lari ke sini adalah nilai
i
yang off per satu untuk apa yang harus menjadi.Sistem yang lebih kompleks dengan struktur data jauh lebih mungkin menghasilkan data yang rusak, atau eksekusi yang tidak tepat.
sumber
atomic
variabel dalam kode yang tidak menulis variabel-variabel itu. misalif (cond) foo=1;
tidak bisa ditransformasikan menjadi asm seperti itufoo = cond ? 1 : foo;
karena load + store (bukan atom RMW) dapat menginjak tulisan dari utas lainnya. Kompiler sudah menghindari hal-hal seperti itu karena mereka ingin berguna untuk menulis program multi-utas, tetapi C ++ 11 membuatnya resmi bahwa kompiler tidak boleh memecahkan kode tempat 2 utas menulisa[1]
dana[2]
Demi kelengkapan dalam kurva pembelajaran; Anda harus menghindari menggunakan variabel global. Anda melakukan pekerjaan dengan baik dengan menjadikannya statis, sehingga akan bersifat lokal ke unit terjemahan.
Berikut ini sebuah contoh:
Langsung di kotak tongkat
sumber
finished
sebagaistatic
dalam blok fungsi. Ini masih akan diinisialisasi hanya sekali, dan jika itu diinisialisasi ke konstanta, ini tidak memerlukan penguncian.finished
juga bisa menggunakanstd::memory_order_relaxed
beban dan toko yang lebih murah ; tidak perlu memesan wrt. variabel lain di kedua utas. Tapi saya tidak yakin saran Davislorstatic
masuk akal; jika Anda memiliki beberapa utas spin-count, Anda tidak perlu ingin menghentikannya dengan flag yang sama. Anda ingin menulis inisialisasifinished
dengan cara yang mengkompilasi hanya inisialisasi, bukan toko atom. (Seperti yang Anda lakukan denganfinished = false;
sintaksis initializer C ++ 17. Godbolt.org/z/EjoKgq ).