Volatile vs. Interlocked vs. lock

671

Katakanlah sebuah kelas memiliki public int counterbidang yang diakses oleh banyak utas. Ini inthanya bertambah atau berkurang.

Untuk menambah bidang ini, pendekatan mana yang harus digunakan, dan mengapa?

  • lock(this.locker) this.counter++;,
  • Interlocked.Increment(ref this.counter);,
  • Ubah pengubah akses countermenjadi public volatile.

Sekarang saya telah menemukan volatile, saya telah menghapus banyak lockpernyataan dan penggunaan Interlocked. Tetapi adakah alasan untuk tidak melakukan ini?

inti
sumber
Baca referensi Threading dalam C # . Ini mencakup seluk beluk pertanyaan Anda. Ketiganya memiliki tujuan dan efek samping yang berbeda.
spoulson
1
simple-talk.com/blogs/2012/01/24/… Anda dapat melihat penggunaan volitable dalam array, saya tidak sepenuhnya memahaminya, tapi itu adalah referensi tambahan untuk apa ini.
eran otzap
50
Ini seperti mengatakan "Saya telah menemukan bahwa sistem sprinkler tidak pernah diaktifkan, jadi saya akan menghapusnya dan menggantinya dengan alarm asap". Alasan untuk tidak melakukan ini adalah karena ini sangat berbahaya dan hampir tidak memberi Anda manfaat . Jika Anda punya waktu untuk mengganti kode, cari cara untuk membuatnya kurang multithread ! Jangan menemukan cara untuk membuat kode multithread lebih berbahaya dan mudah rusak!
Eric Lippert
1
Rumah saya memiliki alat penyiram dan alarm asap. Saat menambahkan penghitung pada satu utas dan membacanya pada yang lain, sepertinya Anda memerlukan kunci (atau yang Saling Bertautan) dan kata kunci yang mudah menguap. Kebenaran?
yoyo
2
@ yoyo Tidak, Anda tidak perlu keduanya.
David Schwartz

Jawaban:

859

Terburuk (tidak akan bekerja)

Ubah pengubah akses countermenjadipublic volatile

Seperti yang orang lain katakan, ini sendiri sebenarnya tidak aman sama sekali. Intinya volatileadalah bahwa banyak utas yang berjalan pada banyak CPU dapat dan akan menyimpan data dan memesan kembali instruksi.

Jika tidak volatile , dan CPU A menambah nilai, maka CPU B mungkin tidak benar-benar melihat nilai tambah itu sampai beberapa waktu kemudian, yang dapat menyebabkan masalah.

Jika ya volatile, ini hanya memastikan dua CPU melihat data yang sama secara bersamaan. Itu tidak menghentikan mereka sama sekali dari interleaving membaca dan menulis operasi yang merupakan masalah yang Anda coba hindari.

Kedua terbaik:

lock(this.locker) this.counter++;

Ini aman untuk dilakukan (asalkan Anda ingat ke locktempat lain yang Anda akses this.counter). Ini mencegah utas lainnya dari mengeksekusi kode lain yang dijaga oleh locker. Menggunakan kunci juga, mencegah masalah penyusunan ulang multi-CPU seperti di atas, yang sangat bagus.

Masalahnya adalah, pengunciannya lambat, dan jika Anda menggunakan kembali lockerdi beberapa tempat lain yang tidak benar-benar terkait maka Anda dapat memblokir utas lainnya tanpa alasan.

Terbaik

Interlocked.Increment(ref this.counter);

Ini aman, karena secara efektif membaca, menambah, dan menulis dalam 'satu klik' yang tidak dapat diganggu. Karena itu, ini tidak akan memengaruhi kode lain, dan Anda tidak perlu ingat untuk mengunci di tempat lain juga. Ini juga sangat cepat (seperti yang dikatakan MSDN, pada CPU modern, ini seringkali secara harfiah merupakan instruksi CPU tunggal).

Namun saya tidak sepenuhnya yakin apakah itu bisa mengatasi hal-hal penataan ulang CPU lain, atau jika Anda juga perlu menggabungkan volatile dengan kenaikan.

Catatan Interlocked:

  1. METODE TERKAIT DENGAN AMAN SAATNYA PADA SETIAP JUMLAH INTI ATAU CPU.
  2. Metode yang saling bertautan menerapkan pagar penuh di sekitar instruksi yang mereka jalankan, sehingga pemesanan ulang tidak terjadi.
  3. Metode yang saling bertautan tidak perlu atau bahkan tidak mendukung akses ke bidang volatil , karena volatil ditempatkan setengah pagar di sekitar operasi pada bidang yang diberikan dan saling terkait menggunakan pagar penuh.

Catatan kaki: Apa volatile sebenarnya baik untuk.

Karena volatiletidak mencegah masalah multithreading semacam ini, untuk apa? Contoh yang baik adalah mengatakan Anda memiliki dua utas, satu yang selalu menulis ke variabel (katakanlah queueLength), dan satu yang selalu membaca dari variabel yang sama.

Jika queueLengthtidak mudah menguap, utas A dapat menulis lima kali, tetapi utas B dapat melihat bahwa penulisan tersebut tertunda (atau bahkan berpotensi dalam urutan yang salah).

Sebuah solusi adalah mengunci, tetapi Anda juga bisa menggunakan volatile dalam situasi ini. Ini akan memastikan bahwa utas B akan selalu melihat hal terbaru yang ditulis oleh utas A. Namun perlu dicatat bahwa logika ini hanya berfungsi jika Anda memiliki penulis yang tidak pernah membaca, dan pembaca yang tidak pernah menulis, dan jika hal yang Anda tulis adalah nilai atom. Segera setelah Anda melakukan satu baca-modifikasi-tulis, Anda harus pergi ke operasi Saling Bertautan atau menggunakan Kunci.

Orion Edwards
sumber
29
"Aku tidak sepenuhnya yakin ... jika kamu juga perlu menggabungkan volatile dengan kenaikan." Mereka tidak dapat dikombinasikan AFAIK, karena kita tidak dapat melewati volatile oleh ref. Omong-omong jawaban yang bagus.
Hosam Aly
41
Terima kasih banyak! Catatan kaki Anda pada "Apa volatile sebenarnya baik untuk" adalah apa yang saya cari dan mengkonfirmasi bagaimana saya ingin menggunakan volatile.
Jacques Bosch
7
Dengan kata lain, jika var dinyatakan sebagai volatile, kompiler akan mengasumsikan bahwa nilai var tidak akan tetap sama (yaitu volatile) setiap kali kode Anda menemukan itu. Jadi dalam satu loop seperti: while (m_Var) {}, dan m_Var disetel ke false di utas lain, kompiler tidak akan cukup memeriksa apa yang sudah ada dalam register yang sebelumnya dimuat dengan nilai m_Var tetapi membaca nilainya dari m_Var lagi. Namun, itu tidak berarti bahwa tidak menyatakan volatile akan menyebabkan perulangan berjalan tanpa batas - menetapkan volatile hanya menjamin bahwa itu tidak akan terjadi jika m_Var disetel ke false di utas lain.
Zach Saw
35
@Zach Saw: Di bawah model memori untuk C ++, volatile adalah bagaimana Anda menggambarkannya (pada dasarnya berguna untuk memori yang dipetakan perangkat dan tidak banyak yang lain). Di bawah model memori untuk CLR (pertanyaan ini ditandai C #) adalah bahwa volatile akan menyisipkan hambatan memori sekitar membaca dan menulis ke lokasi penyimpanan. Hambatan memori (dan variasi terkunci khusus dari beberapa instruksi perakitan) adalah Anda memberi tahu prosesor untuk tidak menyusun ulang hal-hal, dan itu cukup penting ...
Orion Edwards
19
@ZachSaw: Bidang volatile di C # mencegah kompiler C # dan kompiler jit membuat optimisasi tertentu yang akan men-cache nilai. Itu juga membuat jaminan tertentu tentang apa urutan membaca dan menulis dapat diamati berada di beberapa utas. Sebagai detail implementasi, hal itu dapat dilakukan dengan memperkenalkan hambatan memori pada membaca dan menulis. Semantik yang tepat dijamin dijelaskan dalam spesifikasi; perhatikan bahwa spesifikasi tidak menjamin bahwa urutan yang konsisten dari semua penulisan dan pembacaan yang tidak stabil akan diamati oleh semua utas.
Eric Lippert
147

EDIT: Seperti yang tercantum dalam komentar, hari ini saya senang menggunakan Interlockeduntuk kasus-kasus variabel tunggal di mana itu jelas oke Ketika semakin rumit, saya masih akan kembali ke penguncian ...

Menggunakan volatiletidak akan membantu ketika Anda perlu menambah - karena membaca dan menulis adalah instruksi terpisah. Utas lain dapat mengubah nilai setelah Anda membaca tetapi sebelum Anda menulis kembali.

Secara pribadi saya hampir selalu hanya mengunci - lebih mudah untuk mendapatkan dengan cara yang jelas benar daripada volatilitas atau Interlocked. Sejauh yang saya ketahui, multi-threading bebas kunci adalah untuk para ahli threading sungguhan, di mana saya bukan salah satunya. Jika Joe Duffy dan timnya membangun perpustakaan yang bagus yang akan memparalelkan berbagai hal tanpa mengunci sebanyak yang saya bangun, itu luar biasa, dan saya akan menggunakannya dalam sekejap - tetapi ketika saya melakukan threading sendiri, saya mencoba untuk tetap sederhana.

Jon Skeet
sumber
16
+1 untuk memastikan saya melupakan pengkodean bebas kunci dari sekarang.
Xaqron
5
kode bebas kunci jelas tidak benar-benar bebas kunci karena mereka mengunci pada tahap tertentu - baik di tingkat (FSB) bus atau interCPU, masih ada penalti yang harus Anda bayar. Namun penguncian pada tingkat yang lebih rendah ini umumnya lebih cepat selama Anda tidak menjenuhkan bandwidth di mana kunci itu terjadi.
Zach Saw
2
Tidak ada yang salah dengan Interlocked, itu persis apa yang Anda cari dan lebih cepat daripada kunci penuh ()
Jaap
5
@ Jaap: Ya, hari ini saya akan menggunakan saling bertautan untuk penghitung tunggal asli. Saya hanya tidak ingin mulai bermain-main untuk mencoba interaksi antara beberapa pembaruan bebas kunci ke variabel.
Jon Skeet
6
@ZachSaw: Komentar kedua Anda mengatakan bahwa operasi yang saling terkait "mengunci" pada tahap tertentu; istilah "kunci" umumnya menyiratkan bahwa satu tugas dapat mempertahankan kontrol eksklusif sumber daya untuk jangka waktu yang tidak terbatas; keuntungan utama dari pemrograman bebas-kunci adalah bahwa ia menghindari bahaya sumber daya menjadi tidak dapat digunakan sebagai akibat dari tugas yang dimiliki mendapatkan waylaid. Sinkronisasi bus yang digunakan oleh kelas yang saling bertautan bukan hanya "umumnya lebih cepat" - pada sebagian besar sistem memiliki waktu kasus terburuk yang terbatas, sedangkan kunci tidak.
supercat
44

" volatile" tidak diganti Interlocked.Increment! Itu hanya memastikan bahwa variabel tidak di-cache, tetapi digunakan secara langsung.

Menambah variabel sebenarnya membutuhkan tiga operasi:

  1. Baca
  2. kenaikan
  3. menulis

Interlocked.Increment melakukan ketiga bagian sebagai operasi atom tunggal.

Michael Damatov
sumber
4
Dengan kata lain, perubahan yang saling terkait sepenuhnya dipagari dan karenanya bersifat atomik. Anggota yang mudah menguap hanya dipagari sebagian dan karenanya tidak dijamin aman dari utas.
JoeGeeky
1
Sebenarnya, volatiletidak tidak pastikan variabel tidak di-cache. Itu hanya menempatkan pembatasan pada bagaimana itu bisa di-cache. Sebagai contoh, masih bisa di-cache dalam hal-hal cache L2 CPU karena mereka dibuat koheren dalam perangkat keras. Itu masih bisa diutamakan. Menulis masih dapat diposting ke cache, dan sebagainya. (Yang menurut saya adalah tujuan Zach.)
David Schwartz
42

Penguncian atau penambahan yang saling terkait adalah yang Anda cari.

Volatile jelas bukan yang Anda cari - ia hanya memberitahu kompiler untuk memperlakukan variabel sebagai selalu berubah bahkan jika jalur kode saat ini memungkinkan kompiler untuk mengoptimalkan pembacaan dari memori sebaliknya.

misalnya

while (m_Var)
{ }

jika m_Var disetel ke false di utas lain tetapi tidak dinyatakan sebagai volatil, kompiler bebas untuk membuatnya menjadi infinite loop (tetapi tidak berarti selalu akan) dengan membuatnya memeriksa terhadap register CPU (mis. EAX karena itu dulu apa m_Var diambil dari awal) alih-alih mengeluarkan bacaan lain ke lokasi memori m_Var (ini mungkin di-cache - kita tidak tahu dan tidak peduli dan itulah titik koherensi cache x86 / x64). Semua posting sebelumnya oleh orang lain yang menyebutkan perintah reordering hanya menunjukkan mereka tidak mengerti arsitektur x86 / x64. Volatile tidakmasalah baca / tulis hambatan seperti yang tersirat oleh posting sebelumnya mengatakan 'itu mencegah pemesanan ulang'. Faktanya, terima kasih lagi ke protokol MESI, kami dijamin hasil yang kami baca selalu sama di semua CPU terlepas dari apakah hasil sebenarnya telah dipensiunkan ke memori fisik atau hanya tinggal di cache CPU lokal. Saya tidak akan masuk terlalu jauh ke rincian ini tetapi yakinlah bahwa jika ini salah, Intel / AMD kemungkinan akan mengeluarkan penarikan prosesor! Ini juga berarti bahwa kita tidak harus peduli dengan eksekusi yang tidak sesuai, dll. Hasil selalu dijamin untuk pensiun - jika tidak, kita diisi!

Dengan Interlocked Increment, prosesor harus keluar, mengambil nilai dari alamat yang diberikan, lalu menambahkan dan menulisnya kembali - semuanya sambil memiliki kepemilikan eksklusif atas seluruh baris cache (kunci xadd) untuk memastikan tidak ada prosesor lain yang dapat memodifikasi nilainya.

Dengan volatile, Anda masih akan mendapatkan hanya 1 instruksi (dengan asumsi JIT efisien sebagaimana mestinya) - inc dword ptr [m_Var]. Namun, prosesor (cpuA) tidak meminta kepemilikan eksklusif dari garis cache saat melakukan semua yang dilakukan dengan versi yang saling terkait. Seperti yang dapat Anda bayangkan, ini berarti prosesor lain dapat menulis nilai yang diperbarui kembali ke m_Var setelah dibaca oleh cpuA. Jadi alih-alih sekarang telah meningkatkan nilainya dua kali, Anda berakhir hanya dengan sekali.

Semoga ini bisa menyelesaikan masalah.

Untuk info lebih lanjut, lihat 'Memahami Dampak Teknik Kunci Rendah di Aplikasi Multithreaded' - http://msdn.microsoft.com/en-au/magazine/cc163715.aspx

ps Apa yang mendorong balasan yang sangat terlambat ini? Semua balasan sangat salah (terutama yang ditandai sebagai jawaban) dalam penjelasan mereka, saya hanya perlu menjelaskannya kepada orang lain yang membaca ini. mengangkat bahu

pps Saya berasumsi bahwa targetnya adalah x86 / x64 dan bukan IA64 (ia memiliki model memori yang berbeda). Perhatikan bahwa spesifikasi ECMA Microsoft kacau karena menentukan model memori terlemah dan bukan yang terkuat (selalu lebih baik untuk menentukan terhadap model memori terkuat sehingga konsisten di seluruh platform - jika tidak kode yang akan berjalan 24-7 pada x86 / x64 tidak dapat berjalan sama sekali pada IA64 meskipun Intel telah menerapkan model memori yang sama kuat untuk IA64) - Microsoft mengakui hal ini sendiri - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx .

Zach Saw
sumber
3
Menarik. Bisakah Anda referensi ini? Saya dengan senang hati memilih ini, tetapi memposting dengan bahasa agresif 3 tahun setelah jawaban yang sangat sesuai dengan sumber daya yang saya baca akan membutuhkan bukti yang lebih nyata.
Steven Evers
Jika Anda dapat menunjuk ke bagian mana yang ingin Anda rujuk, saya akan dengan senang hati menggali beberapa barang dari suatu tempat (saya sangat ragu saya telah memberikan rahasia dagang vendor x86 / x64 mana saja, jadi ini harus dengan mudah tersedia di wiki, Intel PRM (manual referensi programmer), blog MSFT, MSDN atau yang serupa) ...
Zach Saw
2
Mengapa ada orang yang ingin mencegah CPU dari caching ada di luar saya. Seluruh real estat (jelas tidak dapat diabaikan dalam ukuran dan biaya) yang didedikasikan untuk melakukan koherensi cache benar-benar terbuang jika itu masalahnya ... Kecuali Anda tidak memerlukan koherensi cache, seperti kartu grafis, perangkat PCI dll, Anda tidak akan mengatur garis cache untuk write-through.
Zach Saw
4
Ya, semua yang Anda katakan adalah jika tidak 100% setidaknya 99% sesuai sasaran. Situs ini (kebanyakan) cukup berguna ketika Anda sedang terburu-buru mengembangkannya di tempat kerja tetapi sayangnya keakuratan jawaban yang sesuai dengan (permainan) suara tidak ada. Jadi pada dasarnya di stackoverflow Anda bisa mendapatkan perasaan tentang apa yang menjadi pemahaman populer pembaca bukan apa itu sebenarnya. Terkadang jawaban teratas hanyalah omong kosong belaka - mitos sejenis. Dan sayangnya inilah yang berkembang biak pada orang-orang yang menemukan membaca sambil memecahkan masalah. Ini bisa dimengerti, tidak ada yang bisa tahu segalanya.
user1416420
1
@ BenVoigt saya bisa melanjutkan dan menjawab tentang semua arsitektur. NET berjalan, tapi itu akan memakan waktu beberapa halaman dan jelas tidak cocok untuk SO. Jauh lebih baik untuk mendidik orang berdasarkan mem-model perangkat keras .NET yang paling banyak digunakan daripada yang sewenang-wenang. Dan dengan komentar saya 'di mana-mana', saya memperbaiki kesalahan yang dilakukan orang dengan menganggap pembilasan / pembatalan cache dll. Mereka membuat asumsi tentang perangkat keras yang mendasarinya tanpa menentukan perangkat keras mana.
Zach Saw
16

Fungsi yang saling bertautan tidak mengunci. Mereka atom, artinya mereka dapat menyelesaikan tanpa kemungkinan perubahan konteks selama kenaikan. Jadi tidak ada kemungkinan kebuntuan atau menunggu.

Saya akan mengatakan bahwa Anda harus selalu lebih suka kunci dan kenaikan.

Volatile berguna jika Anda perlu menulis di satu utas untuk dibaca di yang lain, dan jika Anda ingin optimizer tidak menyusun ulang operasi pada variabel (karena hal-hal terjadi di utas lain yang tidak diketahui oleh optimizer). Ini adalah pilihan ortogonal untuk bagaimana Anda meningkatkan.

Ini adalah artikel yang sangat bagus jika Anda ingin membaca lebih lanjut tentang kode bebas kunci, dan cara yang tepat untuk mendekati menulisnya

http://www.ddj.com/hpc-high-performance-computing/210604448

Lou Franco
sumber
11

kunci (...) berfungsi, tetapi dapat memblokir utas, dan dapat menyebabkan kebuntuan jika kode lain menggunakan kunci yang sama dengan cara yang tidak kompatibel.

Saling bertautan. * Adalah cara yang tepat untuk melakukannya ... apalagi overhead karena CPU modern mendukung ini sebagai primitif.

volatile sendiri tidak benar. Utas yang mencoba mengambil dan kemudian menulis kembali nilai yang dimodifikasi masih dapat bertentangan dengan utas lainnya yang melakukan hal yang sama.

Rob Walker
sumber
8

Saya melakukan beberapa tes untuk melihat bagaimana teori itu bekerja: kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html . Tes saya lebih fokus pada CompareExchnage tetapi hasil untuk Peningkatan serupa. Saling bertautan tidak perlu lebih cepat dalam lingkungan multi-cpu. Ini adalah hasil tes untuk Peningkatan pada server 16 CPU berusia 2 tahun. Ingatlah bahwa tes ini juga melibatkan baca aman setelah peningkatan, yang khas di dunia nyata.

D:\>InterlockVsMonitor.exe 16
Using 16 threads:
          InterlockAtomic.RunIncrement         (ns):   8355 Average,   8302 Minimal,   8409 Maxmial
    MonitorVolatileAtomic.RunIncrement         (ns):   7077 Average,   6843 Minimal,   7243 Maxmial

D:\>InterlockVsMonitor.exe 4
Using 4 threads:
          InterlockAtomic.RunIncrement         (ns):   4319 Average,   4319 Minimal,   4321 Maxmial
    MonitorVolatileAtomic.RunIncrement         (ns):    933 Average,    802 Minimal,   1018 Maxmial
Kenneth Xu
sumber
Sampel kode yang Anda uji sangat sepele - benar-benar tidak masuk akal mengujinya seperti itu! Cara terbaik adalah memahami apa yang sebenarnya dilakukan berbagai metode dan menggunakan metode yang sesuai berdasarkan skenario penggunaan yang Anda miliki.
Zach Saw
@Zach, diskusi bagaimana di sini adalah tentang skenario meningkatkan penghitung dengan cara yang aman. Skenario penggunaan apa lagi yang ada di pikiran Anda atau bagaimana Anda mengujinya? Terima kasih atas komentarnya BTW.
Kenneth Xu
Intinya, ini adalah tes buatan. Anda tidak akan memalu lokasi yang sama yang sering dalam skenario dunia nyata. Jika ya, berarti Anda mengalami hambatan oleh FSB (seperti yang ditunjukkan pada kotak server Anda). Bagaimanapun, lihat balasan saya di blog Anda.
Zach Saw
2
Mencari kembali. Jika bottleneck sebenarnya ada pada FSB, implementasi monitor harus mengamati bottleneck yang sama. Perbedaan nyata adalah bahwa Interlocked melakukan menunggu dan mencoba lagi yang menjadi masalah nyata dengan penghitungan kinerja tinggi. Setidaknya saya berharap komentar saya meningkatkan perhatian bahwa Saling Bertautan tidak selalu merupakan pilihan yang tepat untuk berhitung. Fakta bahwa orang mencari alternatif menjelaskan dengan baik. Anda memerlukan penambah panjang gee.cs.oswosatedu
Kenneth Xu
8

Saya memberi jawaban Jon Skeet kedua dan ingin menambahkan tautan berikut untuk semua orang yang ingin tahu lebih banyak tentang "volatile" dan Saling bertautan:

Atomicity, volatilitas, dan immutabilitas berbeda, bagian satu - (Petualangan Coding Eric Lippert yang Luar Biasa)

Atomitas, volatilitas, dan imutabilitas berbeda, bagian kedua

Atomitas, volatilitas, dan imutabilitas berbeda, bagian ketiga

Sayonara Volatile - (snapshot Wayback Machine dari Joe Duffy, seperti yang muncul pada 2012)

zihotki
sumber
3

Saya ingin menambah disebutkan dalam jawaban yang lain perbedaan antara volatile, Interlockeddan lock:

Kata kunci yang mudah menguap dapat diterapkan ke bidang jenis ini :

  • Jenis referensi.
  • Jenis pointer (dalam konteks yang tidak aman). Perhatikan bahwa meskipun pointer itu sendiri dapat berubah-ubah, objek yang ditunjuknya tidak bisa. Dengan kata lain, Anda tidak dapat mendeklarasikan "pointer" sebagai "volatile".
  • Jenis sederhana seperti sbyte, byte, short, ushort, int, uint, char, float, dan bool.
  • Jenis enum dengan salah satu jenis dasar berikut: byte, sbyte, short, ushort, intatau uint.
  • Parameter tipe generik dikenal sebagai tipe referensi.
  • IntPtrdan UIntPtr.

Tipe lain , termasuk doubledan long, tidak dapat ditandai "volatil" karena membaca dan menulis ke bidang tipe tersebut tidak dapat dijamin sebagai atom. Untuk melindungi akses multi-utas ke jenis bidang tersebut, gunakan Interlockedanggota kelas atau lindungi akses menggunakan lockpernyataan.

Vadim S.
sumber