C ++ 11 memperkenalkan model memori standar. Apa artinya? Dan bagaimana ini akan mempengaruhi pemrograman C ++?


1894

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. :-)


3
@curiousguy: Elaborate ...
Nawaz

4
@curiousguy: Tulis blog lalu ... dan usulkan perbaikan juga. Tidak ada cara lain untuk membuat poin Anda valid dan rasional.
Nawaz

2
Saya salah mengira situs itu sebagai tempat untuk bertanya dan bertukar gagasan. Salahku; itu adalah tempat untuk konformitas di mana Anda tidak dapat tidak setuju dengan Herb Sutter bahkan ketika ia secara terang-terangan menentang dirinya sendiri tentang melempar spec.
curiousguy

5
@curiousguy: C ++ adalah apa yang dikatakan Standard, bukan apa yang dikatakan oleh pria acak di internet. Jadi ya, harus ada kesesuaian dengan Standar. C ++ BUKAN filosofi terbuka di mana Anda dapat berbicara tentang apa pun yang tidak sesuai dengan Standar.
Nawaz

3
"Saya membuktikan bahwa tidak ada program C ++ yang dapat memiliki perilaku yang jelas." . Klaim tinggi, tanpa bukti!
Nawaz

Jawaban:


2205

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:

           Global
           int x, y;

Thread 1            Thread 2
x = 17;             cout << y << " ";
y = 37;             cout << x << endl;

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:

           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;

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), atau 0 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 0sebagai keluaran dari program ini, maka Anda dapat menulis ini:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_relaxed);   cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed);   cout << x.load(memory_order_relaxed) << endl;

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:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_release);   cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release);   cout << x.load(memory_order_acquire) << endl;

Ini membawa kita kembali ke beban dan toko yang dipesan - jadi 37 0bukan 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 0atau 37 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 .


37
Jawaban yang bagus, tetapi ini benar-benar memohon untuk beberapa contoh sebenarnya dari primitif baru. Juga, saya pikir pemesanan memori tanpa primitif sama dengan pra-C ++ 0x: tidak ada jaminan.
John Ripley

5
@ John: Saya tahu, tapi saya sendiri masih belajar primitif :-). Juga saya pikir mereka menjamin akses byte adalah atom (meskipun tidak dipesan) yang mengapa saya pergi dengan "char" untuk contoh saya ... Tapi saya bahkan tidak 100% yakin tentang itu ... Jika Anda ingin menyarankan yang baik " referensi tutorial "Saya akan menambahkannya ke jawaban saya
Nemo

48
@Nawaz: Ya! Akses memori dapat dipesan ulang oleh kompiler atau CPU. Pikirkan tentang (mis.) Cache dan muatan spekulatif. Urutan di mana memori sistem terkena mungkin tidak seperti apa yang Anda kodekan. Compiler dan CPU akan memastikan bahwa pemesanan ulang tidak merusak kode single-threaded . Untuk kode multi-utas, "model memori" mengkarakterisasi kemungkinan pengurutan ulang, dan apa yang terjadi jika dua utas membaca / menulis lokasi yang sama pada saat yang sama, dan bagaimana Anda menggunakan kontrol atas keduanya. Untuk kode single-threaded, model memori tidak relevan.
Nemo

26
@Nawaz, @Nemo - Detail kecil: model memori baru relevan dalam kode single-threaded sejauh ia menentukan ketidaktentuan ekspresi tertentu, seperti 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 .
JohannesD

17
@ AJG85: Bagian 3.6.2 dari draft C ++ 0x spec mengatakan, "Variabel dengan durasi penyimpanan statis (3.7.1) atau durasi penyimpanan utas (3.7.2) harus diinisialisasi nol (8.5) sebelum inisialisasi lain dilakukan tempat." Karena x, y bersifat global dalam contoh ini, mereka memiliki durasi penyimpanan statis dan karenanya akan diinisialisasi nol, saya percaya.
Nemo

345

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"

Model memori intuitif (dan paling restriktif) adalah konsistensi sekuensial (SC) di mana eksekusi multithread harus terlihat seperti interleaving dari eksekusi berurutan dari masing-masing utas konstituen, seolah-olah utasnya digandakan-waktu pada prosesor inti tunggal.

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] 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"

Untuk mesin memori bersama, model konsistensi memori menentukan perilaku yang terlihat secara arsitektur dari sistem memorinya. Kriteria kebenaran untuk perilaku partisi inti prosesor tunggal antara " satu hasil yang benar " dan " banyak alternatif yang salah ". Ini karena arsitektur prosesor mengamanatkan bahwa eksekusi thread mengubah keadaan input yang diberikan menjadi status output tunggal yang terdefinisi dengan baik, bahkan pada core yang tidak berurutan. Model konsistensi memori bersama, bagaimanapun, menyangkut beban dan penyimpanan beberapa utas dan biasanya memungkinkan banyak eksekusi yang benarsementara melarang banyak (lebih) yang salah. Kemungkinan beberapa eksekusi yang benar adalah karena ISA memungkinkan beberapa utas untuk dieksekusi secara bersamaan, seringkali dengan banyak kemungkinan interleavings hukum instruksi dari utas yang berbeda.

Model konsistensi memori yang santai atau lemah dimotivasi oleh kenyataan bahwa sebagian besar pemesanan memori dalam model yang kuat tidak perlu. Jika utas memperbarui sepuluh item data dan kemudian bendera sinkronisasi, programmer biasanya tidak peduli jika item data diperbarui satu sama lain tetapi hanya semua item data diperbarui sebelum bendera diperbarui (biasanya diterapkan menggunakan instruksi FENCE ). Model-model yang santai berusaha untuk menangkap fleksibilitas pemesanan yang meningkat ini dan hanya mempertahankan pesanan yang dibutuhkan oleh programmer”Untuk mendapatkan kinerja dan kebenaran SC yang lebih tinggi. Misalnya, dalam arsitektur tertentu, buffer tulis FIFO digunakan oleh masing-masing inti untuk menyimpan hasil toko yang sudah dikomit (pensiun) sebelum menulis hasilnya ke cache. Optimalisasi ini meningkatkan kinerja tetapi melanggar SC. Buffer tulis menyembunyikan latensi servis dari miss toko. Karena toko adalah hal biasa, dapat menghindari mengulur-ulur sebagian besar adalah manfaat penting. Untuk prosesor single-core, buffer tulis dapat dibuat secara arsitektur tidak terlihat dengan memastikan bahwa beban untuk mengatasi A mengembalikan nilai toko terbaru ke A bahkan jika satu atau lebih toko ke A berada di buffer tulis. Ini biasanya dilakukan dengan melewati nilai toko terbaru ke A ke beban dari A, di mana "terbaru" ditentukan oleh urutan program, atau dengan menghentikan beban A jika toko ke A ada di buffer tulis. Ketika banyak core digunakan, masing-masing akan memiliki buffer tulis bypass sendiri. Tanpa buffer tulis, perangkat kerasnya adalah SC, tetapi dengan buffer tulis, tidak, membuat buffer tulis terlihat secara arsitektur dalam prosesor multicore.

Pemesanan ulang toko-toko dapat terjadi jika sebuah inti memiliki buffer tulis non-FIFO yang memungkinkan toko berangkat dalam urutan berbeda dari urutan yang mereka masukkan. Ini mungkin terjadi jika toko pertama ketinggalan dalam cache saat hits kedua atau jika toko kedua dapat bergabung dengan toko sebelumnya (yaitu, sebelum toko pertama). Penataan ulang beban-beban juga dapat terjadi pada inti yang dijadwalkan secara dinamis yang menjalankan instruksi di luar urutan program. Itu bisa berperilaku sama dengan menata ulang toko pada inti lain (Bisakah Anda menemukan contoh interleaving antara dua utas?). Menyusun ulang beban sebelumnya dengan penyimpanan kemudian (pemesanan ulang penyimpanan-toko) dapat menyebabkan banyak perilaku yang salah, seperti memuat nilai setelah melepaskan kunci yang melindunginya (jika toko adalah operasi buka kunci).

Karena koherensi cache dan konsistensi memori kadang-kadang bingung, penting juga untuk memiliki kutipan ini:

Tidak seperti konsistensi, koherensi cache tidak terlihat oleh perangkat lunak atau diperlukan. Koherensi berupaya menjadikan cache dari sistem memori bersama sebagai tidak terlihat secara fungsional seperti cache dalam sistem inti tunggal. Koherensi yang benar memastikan bahwa seorang programmer tidak dapat menentukan apakah dan di mana suatu sistem memiliki cache dengan menganalisis hasil dari beban dan penyimpanan. Ini karena koherensi yang benar memastikan bahwa cache tidak pernah mengaktifkan perilaku fungsional yang baru atau berbeda (programmer mungkin masih dapat menyimpulkan kemungkinan struktur cache menggunakan waktuinformasi). Tujuan utama protokol koherensi cache adalah menjaga invarian satu-penulis-banyak-pembaca (SWMR) untuk setiap lokasi memori. Perbedaan penting antara koherensi dan konsistensi adalah bahwa koherensi ditentukan berdasarkan lokasi per-memori , sedangkan konsistensi ditentukan sehubungan dengan semua lokasi memori.

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.


52
+1 untuk analogi dengan relativitas khusus, saya telah mencoba membuat analogi yang sama sendiri. Terlalu sering saya melihat programmer menyelidiki kode ulir yang mencoba menafsirkan perilaku sebagai operasi di utas yang berbeda yang saling berhubungan satu sama lain dalam urutan tertentu, dan saya harus memberi tahu mereka, tidak, dengan sistem multi-prosesor, gagasan keserentakan antara berbagai <s > kerangka referensi </s> utas sekarang tidak ada artinya. Membandingkan dengan relativitas khusus adalah cara yang baik untuk membuat mereka menghargai kompleksitas masalah.
Pierre Lebeaupin

71
Jadi, haruskah Anda menyimpulkan bahwa Semesta adalah multicore?
Peter K

6
@ PeterK: Tepat :) Dan di sini adalah visualisasi yang sangat bagus dari gambar waktu ini oleh fisikawan Brian Greene: youtube.com/watch?v=4BjGWLJNPcA&t=22m12s Ini adalah "Ilusi Waktu [Dokumenter Lengkap]" pada menit 22 dan 12 detik.
Ahmed Nassar

2
Apakah hanya saya atau dia beralih dari model memori 1D (sumbu horizontal) ke model memori 2D (bidang simultan). Saya menemukan ini agak membingungkan tapi mungkin itu karena saya bukan penutur asli ... Masih sangat menarik dibaca.
Selamat tinggal SE

Anda lupa bagian penting: " dengan menganalisis hasil banyak dan menyimpan " ... tanpa menggunakan informasi waktu yang tepat.
curiousguy

115

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:

  1. Optimalisasi, Ras, dan Model Memori
  2. Memesan - Apa: Memperoleh dan Melepaskan
  3. Pemesanan - Bagaimana: Mutex, Atomics, dan / atau Pagar
  4. Batasan Lain pada Kompiler dan Perangkat Keras
  5. Kode & Kinerja Gen: x86 / x64, IA64, POWER, ARM
  6. Atom santai

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?).


10
Pembicaraan itu memang fantastis, benar-benar bernilai 3 jam Anda akan menghabiskan waktu menontonnya.
ZunTzu

5
@ZunTzu: pada sebagian besar pemutar video Anda dapat mengatur kecepatan ke 1,25, 1,5 atau bahkan 2 kali lipat dari aslinya.
Christian Severin

4
@eran apakah kalian punya slide? tautan pada saluran 9 halaman pembicaraan tidak berfungsi.
athos

2
@athos Saya tidak memilikinya, maaf. Coba hubungi saluran 9, saya tidak berpikir penghapusan itu disengaja (tebakan saya adalah bahwa mereka mendapat tautan dari Herb Sutter, diposting apa adanya, dan dia kemudian menghapus file-file itu; tapi itu hanya spekulasi ...).
eran

75

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::stringketika kita semua bisa menggunakan kelas home-rolled string.

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.


28
Utas posix tidak terbatas pada x86. Memang, sistem pertama yang diimplementasikan mungkin bukan sistem x86. Utas Posix tidak bergantung pada sistem, dan berlaku pada semua platform Posix. Ini juga tidak sepenuhnya benar bahwa itu adalah properti perangkat keras karena utas Posix juga dapat diimplementasikan melalui multitasking kooperatif. Tapi tentu saja sebagian besar masalah threading hanya muncul pada implementasi threading perangkat keras (dan beberapa bahkan hanya pada sistem multiprosesor / multicore).
celtschk

57

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).


2
Memang benar bahwa, ketika jawabannya ditulis, Windows berjalan pada x86 / x64 saja, tetapi Windows berjalan, pada beberapa titik waktu, pada IA64, MIPS, Alpha AXP64, PowerPC dan ARM. Saat ini ia berjalan pada berbagai versi ARM, yang mana ingatannya sangat berbeda dari x86, dan hampir tidak ada yang memaafkan.
Lorenzo Dematté

Tautan itu agak rusak (kata "Dokumentasi Visual Studio 2005 Pensiunan" ). Mau memperbaruinya?
Peter Mortensen

3
Itu tidak benar bahkan ketika jawabannya ditulis.
Ben

" untuk mengakses memori yang sama secara bersamaan " untuk mengakses dengan cara yang bertentangan
curiousguy

27

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).


19
Masalahnya sebelumnya adalah bahwa tidak ada mutex (dalam hal standar C ++). Jadi, satu-satunya jaminan yang Anda berikan adalah oleh produsen mutex, yang baik-baik saja selama Anda tidak memasukkan kode (karena perubahan kecil pada jaminan sulit dikenali). Sekarang kami mendapatkan jaminan yang disediakan oleh standar yang harus portabel antar platform.
Martin York

4
@ Martin: dalam hal apapun, satu hal adalah model memori, dan yang lainnya adalah atom dan threading primitif yang berjalan di atas model memori itu.
ninjalj

4
Juga, maksud saya kebanyakan adalah bahwa sebelumnya sebagian besar tidak ada model memori di tingkat bahasa, itu terjadi pada model memori CPU yang mendasarinya. Sekarang ada model memori yang merupakan bagian dari bahasa inti; OTOH, mutex dan sejenisnya selalu bisa dilakukan sebagai perpustakaan.
ninjalj

3
Ini juga bisa menjadi masalah nyata bagi orang-orang yang mencoba menulis perpustakaan mutex. Ketika CPU, pengontrol memori, kernel, kompiler, dan "pustaka C" semuanya diimplementasikan oleh tim yang berbeda, dan beberapa dari mereka berselisih tentang bagaimana hal ini seharusnya bekerja, yah, kadang-kadang hal itu kami para programmer sistem harus melakukan untuk menyajikan fasad yang cukup untuk tingkat aplikasi tidak menyenangkan sama sekali.
zwol

11
Sayangnya itu tidak cukup untuk menjaga struktur data Anda dengan mutex sederhana jika tidak ada model memori yang konsisten dalam bahasa Anda. Ada berbagai optimisasi kompiler yang masuk akal dalam konteks utas tunggal tetapi ketika beberapa utas dan cpu core ikut bermain, penataan ulang akses memori dan optimasi lainnya dapat menghasilkan perilaku yang tidak terdefinisi. Untuk informasi lebih lanjut, lihat "Utas tidak dapat diimplementasikan sebagai perpustakaan" oleh Hans Boehm: citeseer.ist.psu.edu/viewdoc/…
exDM69

0

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 ++:

atomic<uint64_t> seq; // seqlock representation
int data1, data2;     // this data will be protected by seq

T reader() {
    int r1, r2;
    unsigned seq0, seq1;
    while (true) {
        seq0 = seq;
        r1 = data1; // INCORRECT! Data Race!
        r2 = data2; // INCORRECT!
        seq1 = seq;

        // if the lock didn't change while I was reading, and
        // the lock wasn't held while I was reading, then my
        // reads should be valid
        if (seq0 == seq1 && !(seq0 & 1))
            break;
    }
    use(r1, r2);
}

void writer(int new_data1, int new_data2) {
    unsigned seq0 = seq;
    while (true) {
        if ((!(seq0 & 1)) && seq.compare_exchange_weak(seq0, seq0 + 1))
            break; // atomically moving the lock from even to odd is an acquire
    }
    data1 = new_data1;
    data2 = new_data2;
    seq = seq0 + 2; // release the lock by increasing its value to even
}

Seperti tidak intuitif seperti jahitan pada awalnya, data1dan data2perlu atomic<>. Jika mereka bukan atom, maka mereka dapat dibaca (in reader()) pada waktu yang sama persis seperti saat mereka ditulis (in writer()). Menurut model memori C ++, ini adalah perlombaan meskipun reader()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 dari whileloop in reader().

Membuat atomic<>dan mengaksesnya juga tidak cukup memory_order_relaxed. Alasan untuk ini adalah bahwa membaca dari seq (di reader()) 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_fencedengan memory_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 datavariabel 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 ditambahkan atomic_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.


-2

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.


Saya berbagi keinginan kuat Anda untuk perbaikan desain bahasa, tetapi saya pikir jawaban Anda akan lebih berharga jika dipusatkan pada kasus sederhana, di mana Anda menunjukkan dengan jelas dan eksplisit bagaimana perilaku itu melanggar prinsip-prinsip desain bahasa tertentu. Setelah itu saya akan sangat menyarankan Anda, jika Anda mengizinkan saya, untuk memberikan jawaban itu argumentasi yang sangat baik untuk relevansi masing-masing poin, karena mereka akan dibandingkan dengan relevansi manfaat produktivitas kecil yang dirasakan oleh desain C ++
Matias Haeussler

1
@MatiasHaeussler Saya pikir Anda salah membaca jawaban saya; Saya tidak keberatan dengan definisi fitur C ++ tertentu di sini (saya juga punya banyak kritik tajam tetapi tidak di sini). Saya berpendapat di sini bahwa tidak ada konstruksi yang didefinisikan dengan baik di C ++ (atau C). Seluruh semantik MT adalah kekacauan total, karena Anda tidak memiliki semantik berurutan lagi. (Saya percaya Java MT rusak tetapi kurang.) "Contoh sederhana" akan menjadi hampir semua program MT. Jika Anda tidak setuju, Anda dapat menjawab pertanyaan saya tentang cara membuktikan kebenaran program MT C ++ .
curiousguy

Menarik, saya pikir saya lebih mengerti apa yang Anda maksud setelah membaca pertanyaan Anda. Jika saya benar, Anda merujuk pada ketidakmungkinan mengembangkan bukti untuk kebenaran program C ++ MT . Dalam kasus seperti itu saya akan mengatakan bahwa bagi saya adalah sesuatu yang sangat penting bagi masa depan pemrograman komputer, khususnya untuk kedatangan kecerdasan buatan. Tetapi saya juga akan menunjukkan bahwa bagi sebagian besar orang yang mengajukan pertanyaan dalam stack overflow itu bukan sesuatu yang mereka sadari, dan bahkan setelah memahami apa yang Anda maksudkan dan menjadi tertarik
Matias Haeussler

1
"Haruskah pertanyaan tentang demostrabilitas program komputer diposting di stackoverflow atau di stackexchange (jika tidak ada, di mana)?" Yang ini sepertinya menjadi satu untuk meta stackoverflow, bukan?
Matias Haeussler

1
@MatiasHaeussler 1) C dan C ++ pada dasarnya berbagi "model memori" variabel atom, mutex dan multithreading. 2) Relevansi dalam hal ini adalah tentang manfaat memiliki "model memori". Saya pikir manfaatnya nol karena modelnya tidak sehat.
curiousguy
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.