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 0
sebagai 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 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
atau 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 .