Saya pernah mendengar bahwa i ++ bukanlah pernyataan aman-utas karena dalam perakitan itu mengurangi ke menyimpan nilai asli sebagai temp di suatu tempat, menaikkannya, dan kemudian menggantinya, yang dapat terganggu oleh sakelar konteks.
Namun, saya bertanya-tanya tentang ++ i. Sejauh yang saya tahu, ini akan dikurangi menjadi instruksi perakitan tunggal, seperti 'add r1, r1, 1' dan karena itu hanya satu instruksi, itu tidak akan dapat diinterupsi oleh sakelar konteks.
Adakah yang bisa menjelaskan? Saya berasumsi bahwa platform x86 sedang digunakan.
c++
c
multithreading
samoz
sumber
sumber
Jawaban:
Anda salah dengar. Ini mungkin
"i++"
thread-safe untuk kompiler tertentu dan arsitektur prosesor tertentu tetapi tidak diamanatkan dalam standar sama sekali. Faktanya, karena multi-threading bukan bagian dari standar ISO C atau C ++ (a) , Anda tidak dapat menganggap apa pun sebagai thread-safe berdasarkan apa yang menurut Anda akan dikompilasi.Sangat layak jika
++i
dapat dikompilasi ke urutan arbitrer seperti:load r0,[i] ; load memory into reg 0 incr r0 ; increment reg 0 stor [i],r0 ; store reg 0 back to memory
yang tidak akan aman untuk thread pada CPU (imajiner) saya yang tidak memiliki instruksi penambahan memori. Atau mungkin pintar dan menyusunnya menjadi:
lock ; disable task switching (interrupts) load r0,[i] ; load memory into reg 0 incr r0 ; increment reg 0 stor [i],r0 ; store reg 0 back to memory unlock ; enable task switching (interrupts)
di mana
lock
menonaktifkan danunlock
mengaktifkan interupsi. Namun, meskipun demikian, ini mungkin tidak aman untuk thread dalam arsitektur yang memiliki lebih dari satu CPU yang berbagi memori (yanglock
hanya dapat menonaktifkan interupsi untuk satu CPU).Bahasa itu sendiri (atau pustaka untuk itu, jika tidak dibangun ke dalam bahasa) akan menyediakan konstruksi yang aman untuk thread dan Anda harus menggunakannya daripada bergantung pada pemahaman Anda (atau mungkin kesalahpahaman) tentang kode mesin apa yang akan dihasilkan.
Hal-hal seperti Java
synchronized
danpthread_mutex_lock()
(tersedia untuk C / C ++ pada beberapa sistem operasi) adalah apa yang perlu Anda perhatikan (a) .(a) Pertanyaan ini ditanyakan sebelum standar C11 dan C ++ 11 diselesaikan. Iterasi tersebut sekarang telah memperkenalkan dukungan threading ke dalam spesifikasi bahasa, termasuk tipe data atom (meskipun mereka, dan utas secara umum, bersifat opsional, setidaknya dalam C).
sumber
Anda tidak dapat membuat pernyataan menyeluruh tentang ++ i atau i ++. Mengapa? Pertimbangkan untuk menambah bilangan bulat 64-bit pada sistem 32-bit. Kecuali mesin yang mendasarinya memiliki instruksi quad word "load, increment, store", penambahan nilai itu akan memerlukan banyak instruksi, yang mana pun dapat disela oleh switch konteks thread.
Selain itu,
++i
tidak selalu "tambahkan satu ke nilai". Dalam bahasa seperti C, menaikkan penunjuk sebenarnya menambahkan ukuran benda yang ditunjuk. Artinya, jikai
penunjuk ke struktur 32 byte,++i
tambahkan 32 byte. Sementara hampir semua platform memiliki instruksi "increment value at memory address" yang bersifat atomik, tidak semua memiliki instruksi atomic "add arbitrary value to value at memory address".sumber
Keduanya tidak aman untuk thread.
CPU tidak dapat melakukan matematika secara langsung dengan memori. Itu dilakukan secara tidak langsung dengan memuat nilai dari memori dan melakukan matematika dengan register CPU.
i ++
register int a1, a2; a1 = *(&i) ; // One cpu instruction: LOAD from memory location identified by i; a2 = a1; a1 += 1; *(&i) = a1; return a2; // 4 cpu instructions
++ i
register int a1; a1 = *(&i) ; a1 += 1; *(&i) = a1; return a1; // 3 cpu instructions
Untuk kedua kasus tersebut, ada kondisi balapan yang menghasilkan nilai i yang tidak dapat diprediksi.
Sebagai contoh, mari kita asumsikan ada dua thread ++ i bersamaan dengan masing-masing menggunakan register a1, b1. Dan, dengan pengalihan konteks dijalankan seperti berikut:
Hasilnya, saya tidak menjadi i + 2, itu menjadi i + 1, yang salah.
Untuk mengatasinya, CPU moden menyediakan beberapa jenis LOCK, UNLOCK instruksi cpu selama interval pengalihan konteks dinonaktifkan.
Di Win32, gunakan InterlockedIncrement () untuk melakukan i ++ untuk keamanan thread. Ini jauh lebih cepat daripada mengandalkan mutex.
sumber
Jika Anda bahkan berbagi int lintas utas dalam lingkungan multi-inti, Anda memerlukan batasan memori yang tepat. Ini bisa berarti menggunakan instruksi yang saling bertautan (lihat InterlockedIncrement di win32 misalnya), atau menggunakan bahasa (atau kompiler) yang membuat jaminan keamanan thread tertentu. Dengan pengurutan ulang instruksi level CPU dan cache serta masalah lainnya, kecuali Anda memiliki jaminan tersebut, jangan berasumsi bahwa apa pun yang dibagikan di seluruh utas aman.
Sunting: Satu hal yang dapat Anda asumsikan dengan sebagian besar arsitektur adalah bahwa jika Anda berurusan dengan satu kata yang disejajarkan dengan benar, Anda tidak akan berakhir dengan satu kata pun yang berisi kombinasi dari dua nilai yang digabungkan bersama. Jika dua penulisan terjadi di atas satu sama lain, yang satu akan menang, dan yang lainnya akan dibuang. Jika Anda berhati-hati, Anda dapat memanfaatkan ini, dan melihat bahwa ++ i atau i ++ aman untuk thread dalam situasi penulis tunggal / banyak pembaca.
sumber
Jika Anda menginginkan kenaikan atom dalam C ++, Anda dapat menggunakan pustaka C ++ 0x (
std::atomic
tipe data) atau sesuatu seperti TBB.Dahulu kala pedoman pengkodean GNU mengatakan bahwa memperbarui tipe data yang sesuai dengan satu kata adalah "biasanya aman" tetapi saran itu salah untuk mesin SMP,
salah untuk beberapa arsitektur,dan salah saat menggunakan kompiler pengoptimalan.Untuk memperjelas komentar "memperbarui tipe data satu kata":
Ada kemungkinan dua CPU pada mesin SMP untuk menulis ke lokasi memori yang sama dalam siklus yang sama, dan kemudian mencoba menyebarkan perubahan ke CPU lain dan cache. Meskipun hanya satu kata dari data yang sedang ditulis sehingga penulisan hanya membutuhkan satu siklus untuk diselesaikan, penulisan tersebut juga terjadi secara bersamaan sehingga Anda tidak dapat menjamin penulisan mana yang berhasil. Anda tidak akan mendapatkan sebagian data yang diperbarui, tetapi satu tulisan akan hilang karena tidak ada cara lain untuk menangani kasus ini.
Bandingkan-dan-tukar koordinat yang benar antara beberapa CPU, tetapi tidak ada alasan untuk percaya bahwa setiap penugasan variabel dari tipe data satu kata akan menggunakan bandingkan-dan-tukar.
Dan meskipun kompiler pengoptimalan tidak memengaruhi cara memuat / penyimpanan dikompilasi, ia dapat berubah ketika pemuatan / penyimpanan terjadi, menyebabkan masalah serius jika Anda mengharapkan pembacaan dan penulisan Anda terjadi dalam urutan yang sama seperti yang muncul di kode sumber ( penguncian yang diperiksa dua kali yang paling terkenal tidak berfungsi di vanilla C ++).
CATATAN Jawaban asli saya juga mengatakan bahwa arsitektur Intel 64 bit rusak dalam menangani data 64 bit. Itu tidak benar, jadi saya mengedit jawabannya, tetapi hasil edit saya mengklaim chip PowerPC rusak. Itu benar ketika membaca nilai langsung (yaitu, konstanta) ke dalam register (lihat dua bagian bernama "Memuat penunjuk" di bawah daftar 2 dan daftar 4). Tetapi ada instruksi untuk memuat data dari memori dalam satu siklus (
lmw
), jadi saya telah menghapus bagian jawaban saya itu.sumber
Pada x86 / Windows di C / C ++, Anda tidak boleh menganggapnya aman untuk thread. Anda harus menggunakan InterlockedIncrement () dan InterlockedDecrement () jika Anda memerlukan operasi atom.
sumber
Jika bahasa pemrograman Anda mengatakan apa-apa tentang benang, namun berjalan pada platform multithreaded, bagaimana bisa setiap konstruksi bahasa bisa aman untuk utas?
Seperti yang ditunjukkan orang lain: Anda perlu melindungi akses multithread apa pun ke variabel dengan panggilan khusus platform.
Ada pustaka di luar sana yang mengabstraksi kekhususan platform, dan standar C ++ yang akan datang telah mengadaptasi model memorinya untuk mengatasi utas (dan dengan demikian dapat menjamin keamanan utas).
sumber
Bahkan jika itu direduksi menjadi satu instruksi perakitan, menambah nilai secara langsung di memori, masih belum aman untuk thread.
Saat menambah nilai dalam memori, perangkat keras melakukan operasi "baca-ubah-tulis": ia membaca nilai dari memori, menambahnya, dan menuliskannya kembali ke memori. Perangkat keras x86 tidak memiliki cara untuk menambah memori secara langsung; RAM (dan cache) hanya dapat membaca dan menyimpan nilai, bukan memodifikasinya.
Sekarang misalkan Anda memiliki dua inti terpisah, baik di soket terpisah atau berbagi satu soket (dengan atau tanpa cache bersama). Prosesor pertama membaca nilai, dan sebelum dapat menulis kembali nilai yang diperbarui, prosesor kedua akan membacanya. Setelah kedua prosesor menulis kembali nilainya, nilainya hanya akan bertambah sekali, bukan dua kali.
Ada cara untuk menghindari masalah ini; Prosesor x86 (dan sebagian besar prosesor multi-inti yang akan Anda temukan) dapat mendeteksi jenis konflik ini di perangkat keras dan mengurutkannya, sehingga seluruh urutan baca-ubah-tulis tampak seperti atom. Namun, karena ini sangat mahal, ini hanya dilakukan ketika diminta oleh kode, pada x86 biasanya melalui
LOCK
awalan. Arsitektur lain dapat melakukan ini dengan cara lain, dengan hasil yang serupa; misalnya, load-linked / store-conditional dan atomic Compare-and-swap (prosesor x86 terbaru juga memiliki yang terakhir ini).Perhatikan bahwa menggunakan
volatile
tidak membantu di sini; itu hanya memberitahu compiler bahwa variabel mungkin telah dimodifikasi secara eksternal dan membaca variabel itu tidak boleh di-cache dalam register atau dioptimalkan. Itu tidak membuat kompiler menggunakan atomic primitif.Cara terbaik adalah dengan menggunakan atomic primitif (jika compiler atau library Anda memilikinya), atau lakukan penambahan secara langsung dalam assembly (menggunakan instruksi atomic yang benar).
sumber
Jangan pernah berasumsi bahwa kenaikan akan dikompilasi menjadi operasi atom. Gunakan InterlockedIncrement atau fungsi serupa apa pun yang ada di platform target Anda.
Sunting: Saya baru saja mencari pertanyaan khusus ini dan kenaikan pada X86 bersifat atom pada sistem prosesor tunggal, tetapi tidak pada sistem multiprosesor. Menggunakan awalan kunci dapat menjadikannya atomic, tetapi jauh lebih portabel hanya dengan menggunakan InterlockedIncrement.
sumber
Menurut pelajaran assembly pada x86 ini, Anda dapat menambahkan register ke lokasi memori secara atomis , sehingga kode Anda mungkin secara atomik mengeksekusi '++ i' ou 'i ++'. Tetapi seperti yang dikatakan di posting lain, C ansi tidak menerapkan atomicity ke operasi '++', jadi Anda tidak dapat memastikan apa yang akan dihasilkan oleh compiler Anda.
sumber
Standar 1998 C ++ tidak ada hubungannya dengan utas, meskipun standar berikutnya (karena tahun ini atau tahun depan) tidak. Oleh karena itu, Anda tidak dapat mengatakan apa pun yang cerdas tentang operasi keamanan thread tanpa mengacu pada penerapannya. Ini bukan hanya prosesor yang digunakan, tetapi kombinasi dari compiler, OS, dan model thread.
Dengan tidak adanya dokumentasi yang sebaliknya, saya tidak akan berasumsi bahwa tindakan apa pun adalah thread-safe, terutama dengan prosesor multi-core (atau sistem multi-prosesor). Saya juga tidak akan mempercayai tes, karena masalah sinkronisasi utas cenderung muncul hanya secara tidak sengaja.
Tidak ada yang aman untuk thread kecuali Anda memiliki dokumentasi yang mengatakan itu untuk sistem tertentu yang Anda gunakan.
sumber
Lemparkan saya ke dalam thread penyimpanan lokal; itu bukan atom, tapi kemudian tidak masalah.
sumber
AFAIK, Menurut standar C ++, baca / tulis ke
int
atomic.Namun, semua yang dilakukannya adalah menyingkirkan perilaku tidak terdefinisi yang terkait dengan data race.
Tetapi masih akan ada perlombaan data jika kedua utas mencoba menambah
i
.Bayangkan skenario berikut:
Mari
i = 0
awalnya:Thread A membaca nilai dari memori dan menyimpannya di cache-nya sendiri. Thread A menambah nilai sebesar 1.
Thread B membaca nilai dari memori dan menyimpannya di cache-nya sendiri. Thread B menambah nilai sebesar 1.
Jika ini semua adalah satu utas, Anda akan masuk
i = 2
ke memori.Namun dengan kedua utas, setiap utas menulis perubahannya dan Utas A menulis
i = 1
kembali ke memori, dan utas B menulisi = 1
ke memori.Ini didefinisikan dengan baik, tidak ada kerusakan sebagian atau konstruksi atau segala jenis robekan suatu objek, tetapi itu masih perlombaan data.
Untuk meningkatkan atom
i
Anda dapat menggunakan:std::atomic<int>::fetch_add(1, std::memory_order_relaxed)
Pemesanan santai dapat digunakan karena kami tidak peduli di mana operasi ini berlangsung, yang kami pedulikan adalah bahwa operasi kenaikan adalah atom.
sumber
Anda mengatakan "ini hanya satu instruksi, itu tidak akan dapat diganggu oleh sakelar konteks." - itu semua baik dan bagus untuk satu CPU, tapi bagaimana dengan CPU dual core? Kemudian Anda benar-benar dapat memiliki dua utas yang mengakses variabel yang sama pada saat yang sama tanpa sakelar konteks apa pun.
Tanpa mengetahui bahasanya, jawabannya adalah mengujinya.
sumber
Menurut saya, jika ekspresi "i ++" adalah satu-satunya dalam pernyataan, itu setara dengan "++ i", kompilernya cukup pintar untuk tidak menyimpan nilai temporal, dll. Jadi, jika Anda dapat menggunakannya secara bergantian (jika tidak, Anda menang tidak bertanya mana yang akan digunakan), tidak masalah mana pun yang Anda gunakan karena hampir sama (kecuali untuk estetika).
Bagaimanapun, meskipun operator kenaikan adalah atom, itu tidak menjamin bahwa penghitungan lainnya akan konsisten jika Anda tidak menggunakan kunci yang benar.
Jika Anda ingin bereksperimen sendiri, tulis program di mana N utas bertambah secara bersamaan variabel bersama M kali masing-masing ... jika nilainya kurang dari N * M, maka beberapa kenaikan telah ditimpa. Cobalah dengan preincrement dan postincrement dan beri tahu kami ;-)
sumber
Untuk penghitung, saya merekomendasikan penggunaan idiom bandingkan dan tukar yang tidak mengunci dan aman untuk utas.
Ini dia di Jawa:
public class IntCompareAndSwap { private int value = 0; public synchronized int get(){return value;} public synchronized int compareAndSwap(int p_expectedValue, int p_newValue){ int oldValue = value; if (oldValue == p_expectedValue) value = p_newValue; return oldValue; } } public class IntCASCounter { public IntCASCounter(){ m_value = new IntCompareAndSwap(); } private IntCompareAndSwap m_value; public int getValue(){return m_value.get();} public void increment(){ int temp; do { temp = m_value.get(); } while (temp != m_value.compareAndSwap(temp, temp + 1)); } public void decrement(){ int temp; do { temp = m_value.get(); } while (temp > 0 && temp != m_value.compareAndSwap(temp, temp - 1)); } }
sumber