Apakah cout disinkronkan / aman untuk utas?

112

Secara umum saya berasumsi bahwa aliran tidak disinkronkan, terserah pengguna untuk melakukan penguncian yang sesuai. Namun, apakah hal-hal seperti coutmendapat perlakuan khusus di perpustakaan standar?

Artinya, jika beberapa utas menulis, coutdapatkah mereka merusak coutobjek? Saya memahami bahwa meskipun disinkronkan, Anda masih akan mendapatkan keluaran interleaved acak, tetapi apakah interleaving itu dijamin. Artinya, apakah aman digunakan coutdari banyak utas?

Apakah vendor ini bergantung? Apa yang dilakukan gcc?


Penting : Harap berikan referensi untuk jawaban Anda jika Anda menjawab "ya" karena saya butuh semacam bukti untuk ini.

Perhatian saya juga bukan tentang panggilan sistem yang mendasarinya, itu baik-baik saja, tetapi aliran menambahkan lapisan buffer di atas.

edA-qa mort-ora-y
sumber
2
Ini tergantung vendor. C ++ (sebelum C ++ 0x) tidak memiliki gagasan tentang beberapa utas.
Sven
2
Bagaimana dengan c ++ 0x? Ini mendefinisikan model memori dan apa itu utas, jadi mungkin hal-hal ini menetes di keluaran?
rubenvb
2
Apakah ada vendor yang membuatnya aman untuk thread?
edA-qa mort-ora-y
Ada yang punya link ke standar terbaru C ++ 2011 yang diusulkan?
edA-qa mort-ora-y
4
Dalam beberapa hal, di sinilah printfbersinar saat output lengkap ditulis stdoutdalam satu pengambilan; ketika menggunakan std::coutsetiap tautan rantai ekspresi akan menjadi keluaran secara terpisah stdout; di antara mereka mungkin ada beberapa utas lain yang menulis stdoutkarena urutan hasil akhir menjadi kacau.
legends2k

Jawaban:

106

Standar C ++ 03 tidak mengatakan apa-apa tentang itu. Jika Anda tidak memiliki jaminan tentang keamanan thread dari sesuatu, Anda harus memperlakukannya sebagai tidak aman untuk thread.

Yang menarik di sini adalah fakta yang coutdisangga. Sekalipun panggilan ke write(atau apa pun yang menyelesaikan efek itu dalam implementasi tertentu) dijamin eksklusif satu sama lain, buffer mungkin dibagikan oleh thread yang berbeda. Ini akan dengan cepat menyebabkan korupsi keadaan internal aliran.

Dan bahkan jika akses ke buffer dijamin aman untuk thread, menurut Anda apa yang akan terjadi dalam kode ini?

// in one thread
cout << "The operation took " << result << " seconds.";

// in another thread
cout << "Hello world! Hello " << name << "!";

Anda mungkin ingin setiap baris di sini bertindak dalam pengecualian timbal balik. Tapi bagaimana implementasi bisa menjamin itu?

Di C ++ 11, kami memiliki beberapa jaminan. FDIS mengatakan hal berikut di §27.4.1 [iostream.objects.overview]:

Akses bersamaan ke fungsi input yang diformat dan tidak diformat (§27.7.2.1) dan fungsi output (§27.7.3.1) yang tersinkronisasi (§27.5.3.4) atau aliran C standar oleh beberapa utas tidak boleh mengakibatkan perlombaan data (§ 1.10). [Catatan: Pengguna masih harus menyinkronkan penggunaan objek dan aliran ini secara bersamaan dengan beberapa utas jika mereka ingin menghindari karakter yang disisipkan. - catatan akhir]

Jadi, Anda tidak akan mendapatkan aliran yang rusak, tetapi Anda masih perlu menyinkronkannya secara manual jika Anda tidak ingin keluarannya menjadi sampah.

R. Martinho Fernandes
sumber
2
Secara teknis benar untuk C ++ 98 / C ++ 03, tapi saya pikir semua orang tahu itu. Tapi ini tidak menjawab dua pertanyaan menarik: Bagaimana dengan C ++ 0x? Apa implementasi khas benar-benar lakukan ?
Nemo
1
@ edA-qa mort-ora-y: Tidak, Anda salah. C ++ 11 dengan jelas mendefinisikan bahwa objek aliran standar dapat disinkronkan dan mempertahankan perilaku yang terdefinisi dengan baik, bukan secara default.
ildjarn
12
@ildjarn - Tidak, @ edA-qa mort-ora-y benar. Selama cout.sync_with_stdio()benar, menggunakan coutuntuk mengeluarkan karakter dari beberapa utas tanpa sinkronisasi tambahan sudah ditentukan dengan baik, tetapi hanya pada tingkat byte individu. Jadi, cout << "ab";dan cout << "cd"dieksekusi di utas yang berbeda dapat menghasilkan acdb, misalnya, tetapi mungkin tidak menyebabkan Perilaku Tidak Terdefinisi.
JohannesD
4
@JohannesD: Kami setuju di sana - itu disinkronkan dengan C API yang mendasarinya. Maksud saya adalah bahwa ini tidak "disinkronkan" dengan cara yang berguna, yaitu seseorang masih memerlukan sinkronisasi manual jika mereka tidak menginginkan data sampah.
ildjarn
2
@ildjarn, saya baik-baik saja dengan data sampah, sedikit yang saya mengerti. Saya hanya tertarik dengan kondisi data race yang sepertinya sudah jelas sekarang.
edA-qa mort-ora-y
16

Ini pertanyaan yang bagus.

Pertama, C ++ 98 / C ++ 03 tidak memiliki konsep "utas". Jadi di dunia itu, pertanyaannya tidak ada artinya.

Bagaimana dengan C ++ 0x? Lihat jawaban Martinho (yang saya akui mengejutkan saya).

Bagaimana dengan implementasi spesifik sebelum C ++ 0x? Nah, sebagai contoh, berikut adalah kode sumber basic_streambuf<...>:sputcdari GCC 4.5.2 (header "streambuf"):

 int_type
 sputc(char_type __c)
 {
   int_type __ret;
   if (__builtin_expect(this->pptr() < this->epptr(), true)) {
       *this->pptr() = __c;
        this->pbump(1);
        __ret = traits_type::to_int_type(__c);
      }
    else
        __ret = this->overflow(traits_type::to_int_type(__c));
    return __ret;
 }

Jelas, ini tidak melakukan penguncian. Dan juga tidak xsputn. Dan ini pasti jenis streambuf yang digunakan cout.

Sejauh yang saya tahu, libstdc ++ tidak melakukan penguncian di sekitar operasi streaming mana pun. Dan saya tidak mengharapkan apapun, karena itu akan lambat.

Jadi dengan implementasi ini, jelas dimungkinkan untuk output dua utas saling korup ( tidak hanya menyisipkan).

Mungkinkah kode ini merusak struktur datanya sendiri? Jawabannya tergantung pada kemungkinan interaksi dari fungsi-fungsi ini; misalnya, apa yang terjadi jika satu thread mencoba membersihkan buffer sementara thread lain mencoba memanggil xsputnatau apa pun. Ini mungkin tergantung pada bagaimana kompilator dan CPU Anda memutuskan untuk menyusun ulang beban dan penyimpanan memori; dibutuhkan analisis yang cermat untuk memastikannya. Itu juga tergantung pada apa yang dilakukan CPU Anda jika dua utas mencoba mengubah lokasi yang sama secara bersamaan.

Dengan kata lain, meskipun kebetulan berfungsi dengan baik di lingkungan Anda saat ini, ini mungkin rusak saat Anda memperbarui runtime, compiler, atau CPU Anda.

Ringkasan eksekutif: "Saya tidak akan". Buat kelas logging yang melakukan penguncian yang benar, atau pindah ke C ++ 0x.

Sebagai alternatif yang lemah, Anda dapat menyetel cout ke unbuffered. Kemungkinan (meskipun tidak dijamin) akan melewatkan semua logika yang terkait dengan buffer dan memanggil writesecara langsung. Meskipun itu mungkin sangat lambat.

Nemo
sumber
1
Jawaban yang bagus, tapi lihat respon Martinho yang menunjukkan bahwa C ++ 11 memang mendefinisikan sinkronisasi untuk cout.
edA-qa mort-ora-y
7

C ++ Standard tidak menentukan apakah menulis ke streaming aman untuk thread, tetapi biasanya tidak.

www.techrepublic.com/article/use-stl-streams-for-easy-c-plus-plus-thread-safe-logging

dan juga: Apakah aliran keluaran standar dalam C ++ thread-safe (cout, cerr, clog)?

MEMPERBARUI

Silakan lihat jawaban @Martinho Fernandes untuk mengetahui tentang apa yang dikatakan standar baru C ++ 11 tentang ini.

phoxis
sumber
3
Saya kira karena C ++ 11 sekarang menjadi standar jawaban ini sebenarnya salah sekarang.
edA-qa mort-ora-y
6

Seperti jawaban lain yang disebutkan, ini pasti khusus vendor karena standar C ++ tidak menyebutkan threading (ini berubah dalam C ++ 0x).

GCC tidak menjanjikan banyak hal tentang keamanan thread dan I / O. Tetapi dokumentasi untuk apa yang dijanjikannya ada di sini:

hal utamanya mungkin:

Jenis __basic_file hanyalah kumpulan pembungkus kecil di sekitar lapisan C stdio (sekali lagi, lihat tautan di bawah Struktur). Kami tidak mengunci diri kami sendiri, tetapi hanya melalui panggilan untuk fopen, fwrite, dan sebagainya.

Jadi, untuk 3.0, pertanyaan "apakah multithreading aman untuk I / O" harus dijawab dengan, "apakah thread library C platform Anda aman untuk I / O?" Beberapa secara default, beberapa tidak; banyak yang menawarkan beberapa implementasi pustaka C dengan berbagai pengorbanan keamanan dan efisiensi thread. Anda, programmer, selalu diminta untuk berhati-hati dengan banyak utas.

(Sebagai contoh, standar POSIX mensyaratkan bahwa operasi C stdio FILE * bersifat atomik. Pustaka C yang sesuai dengan POSIX (misalnya, di Solaris dan GNU / Linux) memiliki mutex internal untuk menserialisasikan operasi pada FILE * s. Namun, Anda masih perlu untuk tidak melakukan hal-hal bodoh seperti memanggil fclose (fs) di satu utas yang diikuti dengan akses fs di utas lain.)

Jadi, jika library C platform Anda threadsafe, maka operasi I / O fstream Anda akan menjadi threadsafe di level terendah. Untuk operasi tingkat yang lebih tinggi, seperti memanipulasi data yang terdapat dalam kelas pemformatan aliran (misalnya, menyiapkan callback di dalam std :: ofstream), Anda perlu menjaga akses tersebut seperti sumber daya bersama penting lainnya.

Saya tidak tahu apakah ada yang berubah dari jangka waktu 3.0 yang disebutkan.

Dokumentasi keamanan untaian MSVC untuk iostreamsdapat ditemukan di sini: http://msdn.microsoft.com/en-us/library/c9ceah3b.aspx :

Objek tunggal aman untuk thread untuk membaca dari beberapa thread. Misalnya, dengan objek A, aman untuk membaca A dari utas 1 dan dari utas 2 secara bersamaan.

Jika satu objek sedang ditulis oleh satu utas, semua membaca dan menulis ke objek itu di utas yang sama atau lainnya harus dilindungi. Misalnya, diberi objek A, jika utas 1 menulis ke A, utas 2 harus dicegah dari membaca atau menulis ke A.

Aman untuk membaca dan menulis ke satu instance dari suatu jenis bahkan jika utas lain sedang membaca atau menulis ke instance lain dari jenis yang sama. Misalnya, diberikan objek A dan B dengan tipe yang sama, aman jika A ditulis di utas 1 dan B sedang dibaca di utas 2.

...

Kelas iostream

Kelas iostream mengikuti aturan yang sama dengan kelas lainnya, dengan satu pengecualian. Aman untuk menulis ke objek dari beberapa utas. Misalnya, utas 1 dapat menulis ke cout pada saat yang sama dengan utas 2. Namun, hal ini dapat mengakibatkan keluaran dari kedua utas tersebut saling bercampur.

Catatan: Membaca dari buffer aliran tidak dianggap sebagai operasi baca. Ini harus dianggap sebagai operasi tulis, karena ini mengubah status kelas.

Perhatikan bahwa informasi tersebut adalah untuk versi MSVC terbaru (saat ini untuk VS 2010 / MSVC 10 / cl.exe16.x). Anda dapat memilih informasi untuk versi MSVC yang lebih lama menggunakan kontrol dropdown pada halaman (dan informasinya berbeda untuk versi yang lebih lama).

Michael Burr
sumber
1
"Saya tidak tahu apakah ada yang berubah dari kerangka waktu 3.0 yang disebutkan." Itu pasti terjadi. Selama beberapa tahun terakhir, implementasi aliran g ++ telah melakukan bufferingnya sendiri.
Nemo