list :: empty () perilaku multi-utas?

9

Saya punya daftar yang saya ingin utas berbeda untuk mengambil elemen. Untuk menghindari mengunci mutex yang menjaga daftar ketika kosong, saya periksa empty()sebelum mengunci.

Tidak apa-apa jika panggilan list::empty()tidak 100% benar. Saya hanya ingin menghindari menabrak atau mengganggu panggilan list::push()dan list::pop()panggilan bersamaan .

Apakah saya aman untuk mengasumsikan VC ++ dan Gnu GCC hanya akan terkadang empty()salah dan tidak ada yang lebih buruk?

if(list.empty() == false){ // unprotected by mutex, okay if incorrect sometimes
    mutex.lock();
    if(list.empty() == false){ // check again while locked to be certain
         element = list.back();
         list.pop_back();
    }
    mutex.unlock();
}
Anne Quinn
sumber
1
Tidak, Anda tidak dapat menganggap ini. Anda dapat menggunakan wadah bersamaan seperti concurrent_queue
Panagiotis Kanavos
2
@Fureeish Ini harus menjadi jawaban. Saya akan menambahkan bahwa std::list::sizetelah menjamin kompleksitas waktu yang konstan, yang pada dasarnya berarti bahwa ukuran (jumlah node) perlu disimpan dalam variabel yang terpisah; sebut saja itu size_. std::list::emptymaka kemungkinan mengembalikan sesuatu sebagai size_ == 0, dan membaca dan menulis bersamaan size_akan menyebabkan perlombaan data, oleh karena itu, UB.
Daniel Langr
@DanielLangr Bagaimana "waktu konstan" diukur? Apakah itu panggilan fungsi tunggal atau program lengkap?
curiousguy
1
@curiousguy: DanielLangr menjawab pertanyaan Anda dengan "tidak bergantung pada jumlah node daftar", yaitu definisi pasti dari O (1) yang berarti setiap panggilan dijalankan dalam waktu kurang dari waktu yang konstan, terlepas dari jumlah elemen. en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions Pilihan lain (sampai C ++ 11) akan menjadi linear = O (n) yang berarti, ukuran itu harus menghitung elemen (daftar tertaut), yang akan lebih buruk untuk konkurensi (perlombaan data yang lebih jelas daripada pembacaan / penulisan non-atom di konter).
firda
1
@curiousguy: Mengambil contoh Anda sendiri dengan dV, kompleksitas waktu adalah matematika - batas yang sama. Semua hal ini dapat didefinisikan secara rekursif, atau dalam bentuk "Ada C sedemikian sehingga f (N) <C untuk setiap N" - yaitu definisi O (1) (untuk yang diberikan / setiap HW terdapat konstanta C seperti bahwa algo berakhir kurang dari C-waktu pada input apa pun). Rata-rata diamortisasi , yang berarti bahwa beberapa input mungkin membutuhkan waktu lebih lama untuk diproses (mis. Re-hash / re-alokasi yang diperlukan), tetapi rata-rata tetap konstan (dengan asumsi semua input yang mungkin).
firda

Jawaban:

10

Tidak apa-apa jika panggilan list::empty()tidak 100% benar.

Tidak, ini tidak baik. Jika Anda memeriksa apakah daftar kosong di luar beberapa mekanisme sinkronisasi (mengunci mutex) maka Anda memiliki ras data. Memiliki ras data berarti Anda memiliki perilaku yang tidak terdefinisi. Memiliki perilaku yang tidak terdefinisi berarti kita tidak dapat lagi alasan tentang program dan output yang Anda dapatkan adalah "benar".

Jika Anda menghargai kewarasan Anda, Anda akan mendapatkan hit kinerja dan mengunci mutex sebelum memeriksa. Yang mengatakan, daftar itu mungkin bahkan bukan wadah yang tepat untuk Anda. Jika Anda dapat memberi tahu kami apa yang Anda lakukan dengannya, kami mungkin dapat menyarankan wadah yang lebih baik.

NathanOliver
sumber
Perspektif pribadi, panggilan list::empty()adalah tindakan baca yang tidak ada hubungannya denganrace-condition
Ngọc Khánh Nguyễn
3
@ NgọcKhánhNguyễn Jika mereka menambahkan elemen ke dalam daftar maka itu pasti akan menyebabkan perlombaan data saat Anda menulis dan membaca ukuran pada saat yang sama.
NathanOliver
6
@ NgọcKhánhNguyễn Itu salah. Kondisi lomba adalah read-writeatau write-write. Jika Anda tidak percaya saya, memberikan bagian standar pada ras data yang dibaca
NathanOliver
1
@ NgọcKhánhNguyễn: Karena baik tulisan maupun bacaan tidak dijamin atomik, oleh karena itu dapat dieksekusi secara bersamaan, oleh karena itu bacaan tersebut dapat mendapatkan sesuatu yang sama sekali salah (disebut torn read). Bayangkan menulis mengubah 0x00FF ke 0x0100 dalam sedikit endian 8-bit MCU, dimulai dengan menulis ulang rendah 0xFF menjadi 0x00 dan membaca sekarang mendapatkan tepat nol, membaca kedua byte (menulis thread melambat atau ditangguhkan), menulis terus dengan memperbarui byte tinggi ke 0x01, tetapi utas bacaan sudah mendapatkan nilai yang salah (0x00FF, atau 0x0100 tetapi 0x0000 tidak terduga).
firda
1
@ NgọcKhánhNguyễn Mungkin pada beberapa arsitektur tetapi mesin virtual C ++ tidak menawarkan jaminan seperti itu. Bahkan jika perangkat keras Anda melakukannya, akan sah bagi kompiler untuk mengoptimalkan kode dengan cara yang Anda tidak akan pernah melihat perubahan karena kecuali ada sinkronisasi utas, ia dapat menganggap itu berjalan hanya dengan satu utas dan mengoptimalkannya.
NathanOliver
6

Ada membaca dan menulis (kemungkinan besar untuk sizeanggota std::list, jika kita berasumsi bahwa itu dinamai seperti itu) yang tidak disinkronkan dalam reagard satu sama lain . Bayangkan bahwa satu utas memanggil empty()(di bagian luar Anda if()) sedangkan utas lainnya masuk ke dalam if()dan mengeksekusi pop_back(). Anda kemudian membaca variabel yang, mungkin, sedang dimodifikasi. Ini adalah perilaku yang tidak terdefinisi.

Fureeish
sumber
2

Sebagai contoh bagaimana segala sesuatunya salah:

Kompiler yang cukup pintar dapat melihat bahwa mutex.lock()tidak mungkin mengubah nilai list.empty()kembali dan dengan demikian melewatkan ifpemeriksaan dalam sepenuhnya, akhirnya mengarah pop_backpada daftar yang unsur terakhirnya dihapus setelah yang pertama if.

Kenapa bisa begitu? Tidak ada sinkronisasi di list.empty(), jadi jika itu diubah secara bersamaan yang akan membentuk perlombaan data. Standar mengatakan bahwa program tidak boleh memiliki data balapan, sehingga kompiler akan menerima begitu saja (jika tidak, ia dapat melakukan hampir tidak ada optimasi sama sekali). Oleh karena itu dapat mengasumsikan perspektif single-threaded pada yang tidak disinkronkan list.empty()dan menyimpulkan bahwa itu harus tetap konstan.

Ini hanya satu dari beberapa optimasi (atau perilaku perangkat keras) yang dapat merusak kode Anda.

Max Langhof
sumber
Kompiler saat ini bahkan tampaknya tidak ingin mengoptimalkan a.load()+a.load()...
curiousguy
1
@curiousguy Bagaimana hal itu dioptimalkan? Anda meminta konsistensi berurutan penuh di sana, sehingga Anda akan mendapatkannya ...
Max Langhof
@ MaxLanghof Anda tidak berpikir optimasi a.load()*2itu jelas? Bahkan a.load(rel)+b.load(rel)-a.load(rel)tidak dioptimalkan. Tidak ada yang. Mengapa Anda berharap kunci (yang pada dasarnya sebagian besar memiliki konsistensi seq) lebih dioptimalkan?
curiousguy
@curiousguy Karena pemesanan memori akses non-atom (di sini sebelum dan setelah kunci) dan atom sama sekali berbeda? Saya tidak berharap kunci dioptimalkan "lebih", saya berharap bahwa akses yang tidak disinkronkan dioptimalkan lebih dari akses yang konsisten secara berurutan. Kehadiran kunci tidak relevan untuk poin saya. Dan tidak, compiler tidak diperbolehkan untuk mengoptimalkan a.load() + a.load()untuk 2 * a.load(). Jangan ragu untuk bertanya tentang hal ini jika Anda ingin tahu lebih banyak.
Max Langhof
@ MaxLanghof Saya tidak tahu apa yang Anda coba katakan. Kunci pada dasarnya konsisten secara berurutan. Mengapa implementasi akan mencoba melakukan optimasi pada beberapa primitif threading (kunci) dan bukan yang lain (atom)? Apakah Anda berharap akses non-atom dioptimalkan di sekitar penggunaan atom?
curiousguy