Saya sedang belajar re-entrancy dalam pemrograman. Di situs IBM ini (sangat bagus). Saya telah menemukan kode, disalin di bawah. Ini adalah kode pertama yang datang ke situs web.
Kode mencoba menunjukkan masalah yang melibatkan akses bersama ke variabel dalam pengembangan program teks (asinkronisitas) yang tidak linier dengan mencetak dua nilai yang terus berubah dalam "konteks berbahaya".
#include <signal.h>
#include <stdio.h>
struct two_int { int a, b; } data;
void signal_handler(int signum){
printf ("%d, %d\n", data.a, data.b);
alarm (1);
}
int main (void){
static struct two_int zeros = { 0, 0 }, ones = { 1, 1 };
signal (SIGALRM, signal_handler);
data = zeros;
alarm (1);
while (1){
data = zeros;
data = ones;
}
}
Masalah muncul ketika saya mencoba menjalankan kode (atau lebih baik, tidak muncul). Saya menggunakan versi gcc 6.3.0 20170516 (Debian 6.3.0-18 + deb9u1) dalam konfigurasi default. Output yang salah arah tidak terjadi. Frekuensi dalam mendapatkan nilai pasangan "salah" adalah 0!
Apa yang sedang terjadi? Mengapa tidak ada masalah dalam re-entrancy menggunakan variabel global statis?
Jawaban:
Itu tidak benar-benar ulang entrancy ; Anda tidak menjalankan fungsi dua kali di utas yang sama (atau di utas berbeda). Anda bisa mendapatkannya melalui rekursi atau mengirimkan alamat fungsi saat ini sebagai fungsi-pointer callback ke fungsi lain. (Dan itu tidak akan tidak aman karena akan sinkron).
Ini hanyalah perlombaan data vanila UB (Undefined Behavior) antara pengatur sinyal dan utas utama: hanya
sig_atomic_t
dijamin aman untuk ini . Orang lain mungkin bekerja, seperti dalam kasus Anda di mana objek 8-byte dapat dimuat atau disimpan dengan satu instruksi pada x86-64, dan kompiler kebetulan memilih asm itu. (Seperti yang ditunjukkan oleh jawaban icarus).Lihat pemrograman MCU - optimasi C ++ O2 rusak sementara loop - pengendali interupsi pada mikrokontroler inti tunggal pada dasarnya adalah hal yang sama dengan pengendali sinyal dalam program berulir tunggal. Dalam hal ini hasil dari UB adalah bahwa beban diangkat dari loop.
Test-case robek Anda benar-benar terjadi karena perlombaan data UB mungkin dikembangkan / diuji dalam mode 32-bit, atau dengan kompiler dumber yang lebih lama yang memuat anggota struct secara terpisah.
Dalam kasus Anda, kompiler dapat mengoptimalkan toko keluar dari infinite loop karena tidak ada program bebas UB yang bisa mengamatinya.
data
bukan_Atomic
atauvolatile
, dan tidak ada efek samping lain dalam loop. Jadi tidak mungkin pembaca dapat melakukan sinkronisasi dengan penulis ini. Ini sebenarnya terjadi jika Anda mengkompilasi dengan optimasi diaktifkan ( Godbolt menunjukkan loop kosong di bagian bawah utama). Saya juga mengubah struct menjadi dualong long
, dan gcc menggunakan tokomovdqa
16-byte tunggal sebelum loop. (Ini tidak dijamin atom, tetapi dalam praktiknya di hampir semua CPU, dengan asumsi itu disejajarkan, atau pada Intel tidak hanya melewati batas cache-line. Mengapa penugasan integer pada atom variabel sejajar alami pada x86? )Jadi kompilasi dengan optimasi yang diaktifkan juga akan merusak pengujian Anda, dan menunjukkan nilai yang sama setiap kali. C bukan bahasa rakitan portabel.
volatile struct two_int
juga akan memaksa kompiler untuk tidak mengoptimalkannya, tetapi tidak akan memaksanya untuk memuat / menyimpan seluruh struktur secara atom. (Ini tidak akan berhenti dari melakukannya baik, meskipun.) Perhatikan bahwavolatile
tidak tidak menghindari UB data ras, tetapi dalam prakteknya itu cukup untuk komunikasi antar-benang dan bagaimana orang-orang membangun teori atom linting tangan (bersama dengan asm inline) sebelum C11 / C ++ 11, untuk arsitektur CPU normal. Mereka cache-koheren sehinggavolatile
adalah dalam prakteknya sebagian besar mirip dengan_Atomic
denganmemory_order_relaxed
untuk murni-beban dan murni-toko, jika digunakan untuk jenis mempersempit cukup bahwa kompiler akan menggunakan instruksi tunggal sehingga Anda tidak mendapatkan robek. Dan tentu sajavolatile
tidak memiliki jaminan dari standar ISO C vs. kode penulisan yang mengkompilasi dengan asm yang sama menggunakan_Atomic
dan mo_relaxed.Jika Anda memiliki fungsi yang dilakukan
global_var++;
padaint
ataulong long
yang Anda jalankan dari main dan secara tidak sinkron dari penangan sinyal, itu akan menjadi cara untuk menggunakan re-entrancy untuk membuat data-race UB.Bergantung pada bagaimana itu dikompilasi (ke inc tujuan memori atau menambah, atau untuk memisahkan beban / inc / store) itu akan atomik atau tidak sehubungan dengan penangan sinyal di utas yang sama. Lihat Bisakah num ++ menjadi atom untuk 'int num'? untuk lebih lanjut tentang atomicity pada x86 dan di C ++. (C11
stdatomic.h
dan_Atomic
atributnya menyediakan fungsionalitas yang setara denganstd::atomic<T>
templat C ++ 11 )Pengecualian atau interupsi lainnya tidak dapat terjadi di tengah instruksi, jadi add tujuan memori adalah atomic wrt. konteks mengaktifkan CPU single-core. Hanya penulis DMA (koheren cache) yang dapat "menginjak" peningkatan dari
add [mem], 1
tanpalock
awalan pada CPU single-core. Tidak ada core lain yang dapat digunakan untuk menjalankan thread lain.Jadi mirip dengan kasus sinyal: handler sinyal berjalan bukannya eksekusi normal dari benang yang menangani sinyal, sehingga tidak dapat ditangani di tengah satu instruksi.
sumber
Melihat explorer compiler godbolt (setelah menambahkan yang hilang
#include <unistd.h>
), orang melihat bahwa untuk hampir semua kompiler x86_64 kode yang dihasilkan menggunakan gerakan QWORD untuk memuatones
danzeros
dalam satu instruksi.Situs IBM mengatakan
On most machines, it takes several instructions to store a new value in data, and the value is stored one word at a time.
yang mungkin benar untuk CPU pada tahun 2005 tetapi seperti yang ditunjukkan oleh kode tidak benar sekarang. Mengubah struct untuk memiliki dua panjang daripada dua int akan menunjukkan masalah ini.Saya sebelumnya menulis bahwa ini adalah "atom" yang malas. Program ini hanya berjalan pada satu cpu. Setiap instruksi akan lengkap dari sudut pandang cpu ini (dengan asumsi tidak ada lagi yang mengubah memori seperti dma).
Jadi pada
C
tingkat itu tidak didefinisikan bahwa kompiler akan memilih satu instruksi untuk menulis struct, dan korupsi yang disebutkan dalam makalah IBM dapat terjadi. Kompiler modern yang menargetkan CPU saat ini memang menggunakan satu instruksi. Satu instruksi cukup baik untuk menghindari korupsi untuk satu program threaded.sumber
int
menjadilong long
, dan kompilasi ke 32bit. Pelajarannya adalah Anda tidak pernah tahu kapan akan pecah.long long
masih mengkompilasi ke satu instruksi untuk x86-64: 16-bytemovdqa
. Kecuali Anda menonaktifkan pengoptimalan, seperti di tautan Godbolt Anda. (Default GCC adalah-O0
mode debug, yang penuh dengan noise store / reload dan biasanya tidak menarik untuk dilihat.)