C ++ 11 memperkenalkan model memori standar, tetapi apa artinya sebenarnya? Dan bagaimana ini akan mempengaruhi pemrograman C ++?
Artikel ini (oleh Gavin Clarke yang mengutip Herb Sutter ) mengatakan bahwa,
Model memori berarti bahwa kode C ++ sekarang memiliki pustaka standar untuk dipanggil terlepas dari siapa yang membuat kompiler dan pada platform apa ia berjalan. Ada cara standar untuk mengontrol bagaimana utas berbeda berbicara dengan memori prosesor.
"Ketika Anda berbicara tentang pemisahan [kode] di berbagai inti yang ada dalam standar, kami berbicara tentang model memori. Kami akan mengoptimalkannya tanpa melanggar asumsi berikut yang akan dibuat orang dalam kode," kata Sutter .
Yah, saya bisa mengingat ini dan paragraf serupa yang tersedia online (karena saya sudah memiliki model memori sendiri sejak lahir: P) dan bahkan dapat memposting sebagai jawaban atas pertanyaan yang diajukan oleh orang lain, tetapi jujur saja, saya tidak mengerti persis ini.
Pemrogram C ++ pernah menggunakan aplikasi multi-utas sebelumnya, jadi bagaimana masalahnya jika itu adalah thread POSIX, atau utas Windows, atau utas C ++ 11? Apa manfaatnya? Saya ingin memahami detail level rendah.
Saya juga mendapatkan perasaan bahwa model memori C ++ 11 entah bagaimana terkait dengan dukungan multi-threading C ++ 11, karena saya sering melihat keduanya. Jika ya, bagaimana tepatnya? Mengapa mereka harus berhubungan?
Karena saya tidak tahu bagaimana internal multi-threading bekerja, dan apa arti model memori secara umum, tolong bantu saya memahami konsep-konsep ini. :-)
Jawaban:
Pertama, Anda harus belajar berpikir seperti Pengacara Bahasa.
Spesifikasi C ++ tidak membuat referensi ke kompiler, sistem operasi, atau CPU tertentu. Itu membuat referensi ke mesin abstrak yang merupakan generalisasi dari sistem yang sebenarnya. Dalam dunia Pengacara Bahasa, tugas programmer adalah menulis kode untuk mesin abstrak; tugas kompiler adalah mengaktualisasikan kode itu pada mesin beton. Dengan mengkode secara kaku ke spec, Anda dapat yakin bahwa kode Anda akan dikompilasi dan dijalankan tanpa modifikasi pada sistem apa pun dengan kompiler C ++ yang sesuai, baik hari ini atau 50 tahun dari sekarang.
Mesin abstrak dalam spesifikasi C ++ 98 / C ++ 03 pada dasarnya adalah single-threaded. Jadi tidak mungkin untuk menulis kode C ++ multi-threaded yang "sepenuhnya portabel" sehubungan dengan spesifikasi. Spesifikasi tersebut bahkan tidak mengatakan apa-apa tentang atomicity dari penyimpanan dan penyimpanan memori atau urutan di mana banyak dan penyimpanan mungkin terjadi, apalagi hal-hal seperti mutex.
Tentu saja, Anda dapat menulis kode multi-ulir dalam praktiknya untuk sistem beton tertentu - seperti pthreads atau Windows. Tetapi tidak ada cara standar untuk menulis kode multi-utas untuk C ++ 98 / C ++ 03.
Mesin abstrak di C ++ 11 adalah multi-threaded oleh desain. Ini juga memiliki model memori yang terdefinisi dengan baik ; yaitu, ia mengatakan apa yang mungkin dan tidak dapat dilakukan oleh kompiler ketika mengakses memori.
Pertimbangkan contoh berikut, di mana sepasang variabel global diakses secara bersamaan oleh dua utas:
Apa yang mungkin dihasilkan Thread 2?
Di bawah C ++ 98 / C ++ 03, ini bahkan bukan Perilaku Tidak Terdefinisi; pertanyaan itu sendiri tidak ada artinya karena standar tidak merenungkan apa pun yang disebut "utas".
Di bawah C ++ 11, hasilnya adalah Perilaku Tidak Terdefinisi, karena beban dan penyimpanan tidak harus atom secara umum. Yang mungkin tidak tampak seperti perbaikan ... Dan dengan sendirinya, tidak.
Tetapi dengan C ++ 11, Anda dapat menulis ini:
Sekarang segalanya menjadi jauh lebih menarik. Pertama-tama, perilaku di sini didefinisikan . Thread 2 sekarang dapat mencetak
0 0
(jika berjalan sebelum Thread 1),37 17
(jika berjalan setelah Thread 1), atau0 17
(jika berjalan setelah Thread 1 ditetapkan ke x tetapi sebelum ditugaskan ke y).Apa yang tidak dapat dicetak adalah
37 0
, karena mode default untuk muatan / penyimpanan atom di C ++ 11 adalah untuk menegakkan konsistensi berurutan . Ini berarti semua beban dan toko harus "seolah-olah" itu terjadi dalam urutan yang Anda tulis di setiap utas, sementara operasi di antara utas dapat disisipkan di sistem namun suka. Jadi perilaku default atom menyediakan atomisitas dan pemesanan untuk muatan dan penyimpanan.Sekarang, pada CPU modern, memastikan konsistensi berurutan bisa mahal. Khususnya, kompiler kemungkinan akan memancarkan penghalang memori penuh antara setiap akses di sini. Tetapi jika algoritme Anda dapat mentolerir banyak dan penyimpanan yang tidak sesuai pesanan; yaitu, jika memerlukan atomisitas tetapi tidak memesan; yaitu, jika dapat ditoleransi
37 0
sebagai keluaran dari program ini, maka Anda dapat menulis ini:Semakin modern CPU, semakin besar kemungkinan ini lebih cepat dari contoh sebelumnya.
Akhirnya, jika Anda hanya perlu menyimpan barang dan toko tertentu, Anda dapat menulis:
Ini membawa kita kembali ke beban dan toko yang dipesan - jadi
37 0
bukan lagi output yang mungkin - tetapi melakukannya dengan overhead yang minimal. (Dalam contoh sepele ini, hasilnya sama dengan konsistensi sekuensial penuh; dalam program yang lebih besar, itu tidak akan terjadi.)Tentu saja, jika satu-satunya keluaran yang ingin Anda lihat adalah
0 0
atau37 17
, Anda bisa membungkus mutex di sekitar kode asli. Tetapi jika Anda telah membaca sejauh ini, saya yakin Anda sudah tahu cara kerjanya, dan jawaban ini sudah lebih lama dari yang saya maksudkan :-).Jadi, intinya. Mutex itu bagus, dan C ++ 11 membuat standar. Tetapi kadang-kadang karena alasan kinerja Anda ingin primitif tingkat rendah (misalnya, pola penguncian ganda klasik ). Standar baru menyediakan gadget tingkat tinggi seperti mutex dan variabel kondisi, dan juga menyediakan gadget tingkat rendah seperti jenis atom dan berbagai rasa penghalang memori. Jadi sekarang Anda dapat menulis rutin bersamaan yang canggih dan berkinerja tinggi sepenuhnya dalam bahasa yang ditentukan oleh standar, dan Anda dapat yakin bahwa kode Anda akan dikompilasi dan dijalankan tidak berubah baik pada sistem saat ini maupun besok.
Meskipun jujur, kecuali jika Anda adalah seorang ahli dan bekerja pada beberapa kode tingkat rendah yang serius, Anda mungkin harus tetap menggunakan variabel mutex dan kondisi. Itulah yang ingin saya lakukan.
Untuk informasi lebih lanjut tentang hal ini, lihat posting blog ini .
sumber
i = i++
. Konsep lama titik-titik urutan telah dibuang; standar baru menetapkan hal yang sama menggunakan hubungan sequencing-before yang hanya merupakan kasus khusus dari konsep inter-thread yang lebih umum terjadi-sebelum .Saya hanya akan memberikan analogi yang dengannya saya memahami model konsistensi memori (atau model memori, singkatnya). Ini terinspirasi oleh kertas mani Leslie Lamport "Waktu, Jam, dan Pemesanan Acara dalam Sistem Terdistribusi" . Analogi ini tepat dan memiliki signifikansi mendasar, tetapi mungkin berlebihan bagi banyak orang. Namun, saya harap ini memberikan gambar mental (representasi bergambar) yang memfasilitasi alasan tentang model konsistensi memori.
Mari kita lihat sejarah semua lokasi memori dalam diagram ruang-waktu di mana sumbu horizontal mewakili ruang alamat (yaitu, setiap lokasi memori diwakili oleh titik pada sumbu itu) dan sumbu vertikal mewakili waktu (kita akan melihat bahwa, secara umum, tidak ada pengertian universal tentang waktu). Sejarah nilai yang dipegang oleh setiap lokasi memori, oleh karena itu, diwakili oleh kolom vertikal di alamat memori itu. Setiap perubahan nilai adalah karena salah satu utas menulis nilai baru ke lokasi itu. Dengan gambar memori , kami akan berarti agregat / kombinasi nilai semua lokasi memori yang dapat diamati pada waktu tertentu oleh utas tertentu .
Mengutip dari "A Primer pada Memory Consistency dan Cache Coherence"
Urutan memori global itu dapat bervariasi dari satu menjalankan program ke yang lain dan mungkin tidak diketahui sebelumnya. Fitur karakteristik SC adalah himpunan irisan horizontal dalam diagram address-space-time yang mewakili bidang simultan (yaitu, gambar memori). Pada bidang tertentu, semua peristiwa (atau nilai memori) simultan. Ada gagasan tentang Waktu Absolut , di mana semua utas menyetujui nilai memori mana yang simultan. Di SC, setiap saat instan, hanya ada satu gambar memori yang dibagikan oleh semua utas. Itu, pada setiap saat, semua prosesor menyetujui gambar memori (yaitu, konten agregat memori). Ini tidak hanya menyiratkan bahwa semua thread melihat urutan nilai yang sama untuk semua lokasi memori, tetapi juga bahwa semua prosesor mengamati hal yang samakombinasi nilai semua variabel. Ini sama dengan mengatakan semua operasi memori (pada semua lokasi memori) diamati dalam urutan total yang sama oleh semua utas.
Dalam model memori yang rileks, setiap utas akan mengiris alamat-ruang-waktu dengan caranya sendiri, satu-satunya batasan adalah bahwa irisan setiap utas tidak boleh saling bersilangan karena semua utas harus menyetujui sejarah setiap lokasi memori individu (tentu saja , irisan dari utas yang berbeda dapat, dan akan, saling bersilangan). Tidak ada cara universal untuk mengirisnya (tidak ada foliasi alamat-ruang-waktu istimewa). Irisan tidak harus planar (atau linier). Mereka dapat melengkung dan ini adalah apa yang dapat membuat thread membaca nilai-nilai yang ditulis oleh utas lain dari urutan mereka ditulis. Sejarah dari lokasi memori yang berbeda dapat meluncur (atau meregang) secara sewenang-wenang relatif satu sama lain bila dilihat oleh utas tertentu. Setiap utas akan memiliki perasaan yang berbeda tentang peristiwa (atau, yang setara, nilai memori) secara simultan. Set peristiwa (atau nilai memori) yang simultan ke satu utas tidak serempak ke yang lain. Jadi, dalam model memori yang santai, semua utas masih mengamati histori yang sama (yaitu, urutan nilai) untuk setiap lokasi memori. Tetapi mereka dapat mengamati gambar memori yang berbeda (yaitu, kombinasi nilai semua lokasi memori). Bahkan jika dua lokasi memori yang berbeda ditulis oleh utas yang sama secara berurutan, dua nilai yang baru ditulis dapat diamati dalam urutan yang berbeda oleh utas lainnya.
[Gambar dari Wikipedia]
Pembaca yang akrab dengan Teori Relativitas Khusus Einstein akan memperhatikan apa yang saya singgung. Menerjemahkan kata-kata Minkowski ke ranah model memori: ruang alamat dan waktu adalah bayangan alamat-ruang-waktu. Dalam hal ini, setiap pengamat (yaitu, utas) akan memproyeksikan bayang-bayang peristiwa (yaitu, penyimpanan / muatan memori) ke garis dunianya sendiri (yaitu, sumbu waktunya) dan bidang simultanitasnya sendiri (sumbu ruang-alamatnya) . Utas dalam model memori C ++ 11 sesuai dengan pengamat yang bergerak relatif satu sama lain dalam relativitas khusus. Konsistensi berurutan sesuai dengan ruang-waktu Galilea (yaitu, semua pengamat sepakat pada satu urutan peristiwa absolut dan rasa simultanitas global).
Kemiripan antara model memori dan relativitas khusus berasal dari kenyataan bahwa keduanya mendefinisikan serangkaian peristiwa yang dipesan sebagian, sering disebut set kausal. Beberapa peristiwa (mis. Penyimpanan memori) dapat memengaruhi (tetapi tidak terpengaruh oleh) peristiwa lain. Thread C ++ 11 (atau pengamat dalam fisika) tidak lebih dari sebuah rangkaian (yaitu, rangkaian yang terurut total) dari peristiwa (misalnya, memori dimuat dan disimpan ke alamat yang mungkin berbeda).
Dalam relativitas, beberapa urutan dikembalikan ke gambaran yang tampaknya kacau tentang peristiwa yang dipesan sebagian, karena satu-satunya urutan temporal yang disepakati oleh semua pengamat adalah urutan di antara peristiwa-peristiwa "mirip-waktu" (yaitu, peristiwa-peristiwa yang pada prinsipnya dapat dihubungkan oleh partikel yang berjalan lebih lambat) dari kecepatan cahaya dalam ruang hampa). Hanya acara yang berhubungan dengan waktu seperti itu yang secara teratur dipesan. Waktu dalam Fisika, Craig Callender .
Dalam model memori C ++ 11, mekanisme serupa (model konsistensi memperoleh-rilis) digunakan untuk membangun hubungan kausalitas lokal ini .
Untuk memberikan definisi konsistensi memori dan motivasi untuk meninggalkan SC, saya akan mengutip dari "A Primer on Memory Consistency dan Cache Coherence"
Karena koherensi cache dan konsistensi memori kadang-kadang bingung, penting juga untuk memiliki kutipan ini:
Melanjutkan dengan gambaran mental kita, invarian SWMR sesuai dengan persyaratan fisik bahwa ada paling banyak satu partikel yang terletak di satu lokasi tetapi ada pengamat dalam jumlah tidak terbatas di lokasi mana pun.
sumber
Ini sekarang merupakan pertanyaan yang sudah berumur beberapa tahun, tetapi karena sangat populer, ada baiknya menyebutkan sumber yang fantastis untuk mempelajari tentang model memori C ++ 11. Saya melihat tidak ada gunanya meringkas pembicaraannya untuk membuat ini jawaban penuh lain, tetapi mengingat ini adalah orang yang benar-benar menulis standar, saya pikir layak menonton pembicaraan.
Herb Sutter telah berbicara selama tiga jam tentang model memori C ++ 11 berjudul "atomic <> Weapons", tersedia di situs Channel9 - bagian 1 dan bagian 2 . Pembicaraan ini cukup teknis, dan mencakup topik-topik berikut:
Pembicaraan tidak menguraikan API, tetapi lebih pada alasan, latar belakang, di bawah kap dan di belakang layar (apakah Anda tahu semantik santai ditambahkan ke standar hanya karena POWER dan ARM tidak mendukung beban yang disinkronkan secara efisien?).
sumber
Ini berarti bahwa standar sekarang mendefinisikan multi-threading, dan itu mendefinisikan apa yang terjadi dalam konteks banyak utas. Tentu saja, orang menggunakan berbagai implementasi, tapi itu seperti bertanya mengapa kita harus memiliki waktu
std::string
ketika kita semua bisa menggunakan kelas home-rolledstring
.Ketika Anda berbicara tentang utas POSIX atau utas Windows, maka ini sedikit ilusi karena sebenarnya Anda berbicara tentang utas x86, karena ini merupakan fungsi perangkat keras untuk berjalan secara bersamaan. Model memori C ++ 0x membuat jaminan, apakah Anda menggunakan x86, atau ARM, atau MIPS , atau apa pun yang dapat Anda buat.
sumber
Untuk bahasa yang tidak menentukan model memori, Anda menulis kode untuk bahasa dan model memori yang ditentukan oleh arsitektur prosesor. Prosesor dapat memilih untuk memesan kembali akses memori untuk kinerja. Jadi, jika program Anda memiliki balapan data (perlombaan data adalah saat dimungkinkannya beberapa core / hyper-threads untuk mengakses memori yang sama secara bersamaan) maka program Anda tidak lintas platform karena ketergantungannya pada model memori prosesor. Anda dapat merujuk ke manual perangkat lunak Intel atau AMD untuk mengetahui bagaimana prosesor dapat memesan kembali akses memori.
Sangat penting, kunci (dan semantik konkurensi dengan penguncian) biasanya diterapkan dengan cara lintas platform ... Jadi jika Anda menggunakan kunci standar dalam program multithreaded tanpa perlombaan data maka Anda tidak perlu khawatir tentang model memori lintas platform. .
Menariknya, kompiler Microsoft untuk C ++ telah memperoleh / melepaskan semantik untuk volatile yang merupakan ekstensi C ++ untuk menangani kurangnya model memori di C ++ http://msdn.microsoft.com/en-us/library/12a04hfd(v=vs .80) .aspx . Namun, mengingat bahwa Windows hanya berjalan pada x86 / x64 saja, itu tidak mengatakan banyak (model memori Intel dan AMD membuatnya mudah dan efisien untuk menerapkan semantik akuisisi / rilis dalam bahasa).
sumber
Jika Anda menggunakan mutex untuk melindungi semua data Anda, Anda benar-benar tidak perlu khawatir. Mutex selalu menyediakan pemesanan yang memadai dan jaminan visibilitas.
Sekarang, jika Anda menggunakan atom, atau algoritma bebas kunci, Anda perlu memikirkan model memori. Model memori menggambarkan dengan tepat ketika atom memberikan jaminan pemesanan dan visibilitas, dan memberikan pagar portabel untuk jaminan kode tangan.
Sebelumnya, atom akan dilakukan dengan menggunakan kompiler intrinsik, atau pustaka tingkat yang lebih tinggi. Pagar akan dilakukan dengan menggunakan instruksi khusus-CPU (hambatan memori).
sumber
Jawaban di atas mendapatkan aspek paling mendasar dari model memori C ++. Dalam praktiknya, sebagian besar penggunaan
std::atomic<>
"hanya bekerja", setidaknya sampai programmer terlalu mengoptimalkan (misalnya, dengan mencoba bersantai terlalu banyak hal).Ada satu tempat di mana kesalahan masih umum: urutan terkunci . Ada diskusi yang sangat baik dan mudah dibaca tentang tantangan di https://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf . Kunci urutan menarik karena pembaca menghindari penulisan ke kata kunci. Kode berikut didasarkan pada Gambar 1 dari laporan teknis di atas, dan menyoroti tantangan ketika menerapkan kunci urutan di C ++:
Seperti tidak intuitif seperti jahitan pada awalnya,
data1
dandata2
perluatomic<>
. Jika mereka bukan atom, maka mereka dapat dibaca (inreader()
) pada waktu yang sama persis seperti saat mereka ditulis (inwriter()
). Menurut model memori C ++, ini adalah perlombaan meskipunreader()
tidak pernah benar-benar menggunakan data . Selain itu, jika mereka bukan atom, maka kompiler dapat men-cache read pertama dari setiap nilai dalam register. Jelas Anda tidak ingin itu ... Anda ingin membaca kembali di setiap iterasi dariwhile
loop inreader()
.Membuat
atomic<>
dan mengaksesnya juga tidak cukupmemory_order_relaxed
. Alasan untuk ini adalah bahwa membaca dari seq (direader()
) hanya memiliki memperoleh semantik. Secara sederhana, jika X dan Y adalah akses memori, X mendahului Y, X bukan perolehan atau pelepasan, dan Y adalah akuisisi, maka kompiler dapat menyusun ulang Y sebelum X. Jika Y adalah bacaan kedua seq, dan X membaca data, penataan ulang seperti itu akan merusak implementasi kunci.Makalah ini memberikan beberapa solusi. Yang dengan performa terbaik hari ini mungkin yang menggunakan
atomic_thread_fence
denganmemory_order_relaxed
sebelum membaca kedua seqlock. Di koran, ini Gambar 6. Saya tidak mereproduksi kode di sini, karena siapa pun yang telah membaca sejauh ini benar-benar harus membaca koran. Ini lebih tepat dan lengkap daripada posting ini.Masalah terakhir adalah bahwa mungkin tidak wajar untuk membuat
data
variabel atom. Jika Anda tidak dapat memasukkan kode, maka Anda harus sangat berhati-hati, karena casting dari non-atom ke atom hanya sah untuk tipe primitif. C ++ 20 seharusnya ditambahkanatomic_ref<>
, yang akan membuat masalah ini lebih mudah untuk diselesaikan.Untuk meringkas: bahkan jika Anda pikir Anda memahami model memori C ++, Anda harus sangat berhati-hati sebelum menggulung kunci urutan Anda sendiri.
sumber
C dan C ++ dulu didefinisikan oleh jejak eksekusi dari program yang dibentuk dengan baik.
Sekarang mereka setengah didefinisikan oleh jejak eksekusi suatu program, dan setengah posteriori oleh banyak pemesanan pada objek sinkronisasi.
Berarti definisi bahasa ini sama sekali tidak masuk akal karena tidak ada metode logis untuk menggabungkan kedua pendekatan ini. Secara khusus, penghancuran mutex atau variabel atom tidak didefinisikan dengan baik.
sumber