Saya menggunakan Cygwin GCC dan menjalankan kode ini:
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
unsigned u = 0;
void foo()
{
u++;
}
int main()
{
vector<thread> threads;
for(int i = 0; i < 1000; i++) {
threads.push_back (thread (foo));
}
for (auto& t : threads) t.join();
cout << u << endl;
return 0;
}
Disusun dengan baris: g++ -Wall -fexceptions -g -std=c++14 -c main.cpp -o main.o
.
Ini mencetak 1000, yang benar. Namun, saya mengharapkan jumlah yang lebih rendah karena utas menimpa nilai yang sebelumnya bertambah. Mengapa kode ini tidak saling akses?
Mesin uji saya memiliki 4 inti, dan saya tidak membatasi program yang saya ketahui.
Masalah tetap ada saat mengganti konten yang dibagikan foo
dengan sesuatu yang lebih kompleks, misalnya
if (u % 3 == 0) {
u += 4;
} else {
u -= 1;
}
c++
race-condition
mafu
sumber
sumber
u
kembali ke memori. CPU benar-benar akan melakukan hal-hal luar biasa seperti pemberitahuan bahwa garis memori untuku
tidak ada di cache CPU dan itu akan memulai kembali operasi tambahan. Inilah sebabnya mengapa berpindah dari x86 ke arsitektur lain bisa menjadi pengalaman yang membuka mata!while true; do res=$(./a.out); if [[ $res != 1000 ]]; then echo $res; break; fi; done;
mencetak 999 atau 998 pada sistem saya.Jawaban:
foo()
sangat pendek sehingga setiap utas mungkin selesai bahkan sebelum utas berikutnya muncul. Jika Anda menambahkan tidur untuk waktu acakfoo()
sebelumu++
, Anda mungkin mulai melihat apa yang Anda harapkan.sumber
Penting untuk dipahami bahwa kondisi balapan tidak menjamin kode akan berjalan dengan tidak benar, hanya dapat melakukan apa saja, karena ini adalah perilaku yang tidak ditentukan. Termasuk berjalan seperti yang diharapkan.
Khususnya pada mesin X86 dan AMD64, kondisi balapan dalam beberapa kasus jarang menyebabkan masalah karena banyak instruksi bersifat atom dan jaminan koherensi sangat tinggi. Jaminan ini agak berkurang pada sistem multi prosesor di mana awalan kunci diperlukan untuk banyak instruksi menjadi atom.
Jika peningkatan mesin Anda adalah operasi atomik, ini kemungkinan akan berjalan dengan benar meskipun menurut standar bahasa itu adalah Perilaku yang Tidak Ditentukan.
Secara khusus saya berharap dalam kasus ini kode dapat dikompilasi ke instruksi Ambil dan Tambahkan atom (ADD atau XADD dalam perakitan X86) yang memang atom dalam sistem prosesor tunggal, namun pada sistem multiprosesor ini tidak dijamin menjadi atom dan kunci akan diminta untuk membuatnya seperti itu. Jika Anda menjalankan sistem multiprosesor akan ada jendela di mana utas dapat mengganggu dan menghasilkan hasil yang salah.
Secara khusus saya menyusun kode Anda untuk perakitan menggunakan https://godbolt.org/ dan
foo()
mengkompilasi ke:Ini berarti ia hanya melakukan instruksi penambahan yang untuk satu prosesor akan menjadi atom (meskipun seperti yang disebutkan di atas tidak demikian untuk sistem multi prosesor).
sumber
inc [u]
tidak atom. TheLOCK
awalan diperlukan untuk membuat instruksi yang benar-benar atom. OP hanya beruntung. Ingatlah bahwa meskipun Anda memberi tahu CPU "tambahkan 1 ke kata di alamat ini", CPU masih harus mengambil, menambah, menyimpan nilai itu dan CPU lain dapat melakukan hal yang sama secara bersamaan, menyebabkan hasil menjadi salah.Saya pikir tidak apa-apa jika Anda tidur sebelum atau sesudah
u++
. Ini bukan operasi yangu++
diterjemahkan menjadi kode yang - dibandingkan dengan overhead utas pemijahan yang memanggilfoo
- dilakukan dengan sangat cepat sehingga tidak mungkin dicegat. Namun, jika Anda "memperpanjang" operasiu++
, kondisi balapan akan menjadi lebih mungkin:hasil:
694
BTW: Saya juga mencoba
dan itu memberi saya lebih banyak waktu
1997
, tetapi terkadang1995
.sumber
else u -= 1
dieksekusi? Bahkan dalam lingkungan paralel, nilainya tidak boleh tidak cocok%2
, bukan?else u -= 1
dijalankan satu kali, pertama kali foo () dipanggil, ketika u == 0. Sisa 999 kali u ganjil danu += 2
dieksekusi menghasilkan u = -1 + 999 * 2 = 1997; yaitu keluaran yang benar. Kondisi balapan terkadang menyebabkan salah satu dari + = 2 ditimpa oleh utas paralel dan Anda mendapatkan 1995.Itu memang menderita kondisi ras. Masukan
usleep(1000);
sebelumu++;
difoo
dan saya melihat output yang berbeda (<1000) setiap kali.sumber
Jawaban yang mungkin mengapa kondisi balapan tidak terwujud untuk Anda, meskipun memang ada, adalah
foo()
begitu cepat, dibandingkan dengan waktu yang diperlukan untuk memulai utas, sehingga setiap utas selesai bahkan sebelum utas berikutnya dapat dimulai. Tapi...Bahkan dengan versi asli Anda, hasilnya bervariasi berdasarkan sistem: Saya mencobanya dengan cara Anda pada Macbook (quad-core), dan dalam sepuluh kali proses, saya mendapatkan 1000 tiga kali, 999 enam kali, dan 998 kali. Jadi perlombaan agak jarang, tapi jelas ada.
Anda mengompilasi dengan
'-g'
, yang memiliki cara untuk menghilangkan bug. Saya mengkompilasi ulang kode Anda, masih tidak berubah tetapi tanpa'-g'
, dan balapan menjadi jauh lebih jelas: Saya mendapat 1000 sekali, 999 tiga kali, 998 dua kali, 997 dua kali, 996 sekali, dan 992 sekali.Kembali. saran untuk menambahkan tidur - itu membantu, tetapi (a) waktu tidur tetap membuat utas masih miring oleh waktu mulai (tergantung pada resolusi pengatur waktu), dan (b) tidur acak menyebarkannya ketika apa yang kita inginkan adalah tarik mereka lebih dekat. Sebagai gantinya, saya akan mengkodekan mereka untuk menunggu sinyal mulai, jadi saya bisa membuat semuanya sebelum membiarkan mereka mulai bekerja. Dengan versi ini (dengan atau tanpa
'-g'
), saya mendapatkan hasil di semua tempat, serendah 974, dan tidak lebih tinggi dari 998:sumber
-g
bendera tidak dengan cara apapun "make bug menghilang." The-g
bendera pada kedua GNU dan dentang compiler hanya menambahkan simbol debug untuk biner disusun. Ini memungkinkan Anda menjalankan alat diagnostik seperti GDB dan Memcheck pada program Anda dengan beberapa keluaran yang dapat dibaca manusia. Misalnya ketika Memcheck dijalankan di atas program dengan kebocoran memori, Memcheck tidak akan memberi tahu Anda nomor baris kecuali jika program tersebut dibuat menggunakan-g
flag.-O2
bukan dari-g
". Namun demikian, jika Anda belum pernah merasakan kegembiraan dalam berburu bug yang hanya akan terwujud saat dikompilasi tanpa-g
, anggaplah diri Anda beruntung. Itu bisa terjadi, dengan beberapa bug aliasing halus yang paling menjijikkan. Saya telah melihatnya, meskipun baru-baru ini, dan saya dapat percaya mungkin itu adalah kekhasan dari kompiler berpemilik lama, jadi saya akan mempercayai Anda, untuk sementara, tentang versi modern GNU dan Clang.-g
tidak menghentikan Anda untuk menggunakan pengoptimalan. misalnyagcc -O3 -g
membuat sama dengan asmgcc -O3
, tetapi dengan metadata debug. gdb akan mengatakan "dioptimalkan" jika Anda mencoba mencetak beberapa variabel.-g
mungkin dapat mengubah lokasi relatif dari beberapa hal dalam memori, jika salah satu hal yang ditambahkannya merupakan bagian dari.text
bagian. Ini pasti membutuhkan ruang di file objek, tetapi saya pikir setelah menghubungkan semuanya berakhir di salah satu ujung segmen teks (bukan bagian), atau bukan bagian dari segmen sama sekali. Mungkin bisa mempengaruhi di mana hal-hal dipetakan untuk perpustakaan dinamis.