Saya pernah mendengar i ++ tidak aman untuk thread, apakah ++ saya aman untuk thread?

90

Saya pernah mendengar bahwa i ++ bukanlah pernyataan aman-utas karena dalam perakitan itu mengurangi ke menyimpan nilai asli sebagai temp di suatu tempat, menaikkannya, dan kemudian menggantinya, yang dapat terganggu oleh sakelar konteks.

Namun, saya bertanya-tanya tentang ++ i. Sejauh yang saya tahu, ini akan dikurangi menjadi instruksi perakitan tunggal, seperti 'add r1, r1, 1' dan karena itu hanya satu instruksi, itu tidak akan dapat diinterupsi oleh sakelar konteks.

Adakah yang bisa menjelaskan? Saya berasumsi bahwa platform x86 sedang digunakan.

samoz
sumber
Hanya sebuah pertanyaan. Jenis skenario apa yang diperlukan untuk dua (atau lebih) thread mengakses variabel seperti itu? Sejujurnya saya bertanya di sini, bukan mengkritik. Hanya pada jam seperti ini, kepalaku tidak bisa memikirkan apapun.
OscarRyz
5
Variabel kelas dalam kelas C ++ mempertahankan jumlah objek?
paxdiablo
1
Video bagus tentang masalah yang baru saya tonton hari ini karena orang lain memberi tahu saya: youtube.com/watch?v=mrvAqvtWYb4
Johannes Schaub - litb
1
diberi tag ulang sebagai C / C ++; Java tidak dipertimbangkan di sini, C # serupa, tetapi tidak memiliki semantik memori yang didefinisikan secara kaku.
Tim Williscroft
1
@Oscar Reyes Katakanlah Anda memiliki dua utas keduanya menggunakan variabel i. Jika utas satu hanya menambah utas saat berada pada titik tertentu dan utas lainnya hanya mengurangi utas saat berada di titik lain, Anda harus mengkhawatirkan keamanan utas.
samoz

Jawaban:

158

Anda salah dengar. Ini mungkin "i++"thread-safe untuk kompiler tertentu dan arsitektur prosesor tertentu tetapi tidak diamanatkan dalam standar sama sekali. Faktanya, karena multi-threading bukan bagian dari standar ISO C atau C ++ (a) , Anda tidak dapat menganggap apa pun sebagai thread-safe berdasarkan apa yang menurut Anda akan dikompilasi.

Sangat layak jika ++idapat dikompilasi ke urutan arbitrer seperti:

load r0,[i]  ; load memory into reg 0
incr r0      ; increment reg 0
stor [i],r0  ; store reg 0 back to memory

yang tidak akan aman untuk thread pada CPU (imajiner) saya yang tidak memiliki instruksi penambahan memori. Atau mungkin pintar dan menyusunnya menjadi:

lock         ; disable task switching (interrupts)
load r0,[i]  ; load memory into reg 0
incr r0      ; increment reg 0
stor [i],r0  ; store reg 0 back to memory
unlock       ; enable task switching (interrupts)

di mana lockmenonaktifkan dan unlockmengaktifkan interupsi. Namun, meskipun demikian, ini mungkin tidak aman untuk thread dalam arsitektur yang memiliki lebih dari satu CPU yang berbagi memori (yang lockhanya dapat menonaktifkan interupsi untuk satu CPU).

Bahasa itu sendiri (atau pustaka untuk itu, jika tidak dibangun ke dalam bahasa) akan menyediakan konstruksi yang aman untuk thread dan Anda harus menggunakannya daripada bergantung pada pemahaman Anda (atau mungkin kesalahpahaman) tentang kode mesin apa yang akan dihasilkan.

Hal-hal seperti Java synchronizeddan pthread_mutex_lock()(tersedia untuk C / C ++ pada beberapa sistem operasi) adalah apa yang perlu Anda perhatikan (a) .


(a) Pertanyaan ini ditanyakan sebelum standar C11 dan C ++ 11 diselesaikan. Iterasi tersebut sekarang telah memperkenalkan dukungan threading ke dalam spesifikasi bahasa, termasuk tipe data atom (meskipun mereka, dan utas secara umum, bersifat opsional, setidaknya dalam C).

paxdiablo
sumber
8
+1 untuk menekankan bahwa ini bukan masalah khusus platform, belum lagi jawaban yang jelas ...
RBerteig
2
selamat atas lencana perak C Anda :)
Johannes Schaub - litb
Saya pikir Anda harus tepat bahwa tidak ada OS modern yang mengizinkan program mode pengguna untuk mematikan interupsi, dan pthread_mutex_lock () bukan bagian dari C.
Bastien Léonard
@Bastien, tidak ada OS modern yang akan berjalan pada CPU yang tidak memiliki instruksi penambahan memori :-) Tapi maksud Anda diambil tentang C.
paxdiablo
5
@Bastien: Banteng. Prosesor RISC umumnya tidak memiliki instruksi penambahan memori. Tripplet muat / tambah / simpan adalah bagaimana Anda melakukannya di misalnya, PowerPC.
derobert
42

Anda tidak dapat membuat pernyataan menyeluruh tentang ++ i atau i ++. Mengapa? Pertimbangkan untuk menambah bilangan bulat 64-bit pada sistem 32-bit. Kecuali mesin yang mendasarinya memiliki instruksi quad word "load, increment, store", penambahan nilai itu akan memerlukan banyak instruksi, yang mana pun dapat disela oleh switch konteks thread.

Selain itu, ++itidak selalu "tambahkan satu ke nilai". Dalam bahasa seperti C, menaikkan penunjuk sebenarnya menambahkan ukuran benda yang ditunjuk. Artinya, jika ipenunjuk ke struktur 32 byte, ++itambahkan 32 byte. Sementara hampir semua platform memiliki instruksi "increment value at memory address" yang bersifat atomik, tidak semua memiliki instruksi atomic "add arbitrary value to value at memory address".

Jim Mischel
sumber
35
Tentu saja jika Anda tidak membatasi diri Anda pada bilangan bulat 32-bit yang membosankan, dalam bahasa seperti C ++, ++ saya benar-benar dapat menjadi panggilan ke layanan web yang memperbarui nilai dalam database.
Gerhana
16

Keduanya tidak aman untuk thread.

CPU tidak dapat melakukan matematika secara langsung dengan memori. Itu dilakukan secara tidak langsung dengan memuat nilai dari memori dan melakukan matematika dengan register CPU.

i ++

register int a1, a2;

a1 = *(&i) ; // One cpu instruction: LOAD from memory location identified by i;
a2 = a1;
a1 += 1; 
*(&i) = a1; 
return a2; // 4 cpu instructions

++ i

register int a1;

a1 = *(&i) ; 
a1 += 1; 
*(&i) = a1; 
return a1; // 3 cpu instructions

Untuk kedua kasus tersebut, ada kondisi balapan yang menghasilkan nilai i yang tidak dapat diprediksi.

Sebagai contoh, mari kita asumsikan ada dua thread ++ i bersamaan dengan masing-masing menggunakan register a1, b1. Dan, dengan pengalihan konteks dijalankan seperti berikut:

register int a1, b1;

a1 = *(&i);
a1 += 1;
b1 = *(&i);
b1 += 1;
*(&i) = a1;
*(&i) = b1;

Hasilnya, saya tidak menjadi i + 2, itu menjadi i + 1, yang salah.

Untuk mengatasinya, CPU moden menyediakan beberapa jenis LOCK, UNLOCK instruksi cpu selama interval pengalihan konteks dinonaktifkan.

Di Win32, gunakan InterlockedIncrement () untuk melakukan i ++ untuk keamanan thread. Ini jauh lebih cepat daripada mengandalkan mutex.

yogman
sumber
6
"CPU tidak dapat melakukan matematika secara langsung dengan memori" - Ini tidak akurat. Ada CPU-s, di mana Anda dapat melakukan matematika "langsung" pada elemen memori, tanpa perlu memuatnya ke dalam register terlebih dahulu. Misalnya. MC68000
darklon
1
LOCK dan UNLOCK Instruksi CPU tidak ada hubungannya dengan sakelar konteks. Mereka mengunci jalur cache.
David Schwartz
11

Jika Anda bahkan berbagi int lintas utas dalam lingkungan multi-inti, Anda memerlukan batasan memori yang tepat. Ini bisa berarti menggunakan instruksi yang saling bertautan (lihat InterlockedIncrement di win32 misalnya), atau menggunakan bahasa (atau kompiler) yang membuat jaminan keamanan thread tertentu. Dengan pengurutan ulang instruksi level CPU dan cache serta masalah lainnya, kecuali Anda memiliki jaminan tersebut, jangan berasumsi bahwa apa pun yang dibagikan di seluruh utas aman.

Sunting: Satu hal yang dapat Anda asumsikan dengan sebagian besar arsitektur adalah bahwa jika Anda berurusan dengan satu kata yang disejajarkan dengan benar, Anda tidak akan berakhir dengan satu kata pun yang berisi kombinasi dari dua nilai yang digabungkan bersama. Jika dua penulisan terjadi di atas satu sama lain, yang satu akan menang, dan yang lainnya akan dibuang. Jika Anda berhati-hati, Anda dapat memanfaatkan ini, dan melihat bahwa ++ i atau i ++ aman untuk thread dalam situasi penulis tunggal / banyak pembaca.

Gerhana
sumber
Salah secara faktual dalam lingkungan di mana akses int (baca / tulis) bersifat atomik. Ada algoritme yang dapat bekerja di lingkungan seperti itu, meskipun kurangnya hambatan memori mungkin berarti Anda terkadang mengerjakan data yang sudah usang.
MSalters
2
Saya hanya mengatakan bahwa atomicity tidak menjamin keamanan benang. Jika Anda cukup pintar untuk merancang struktur data atau algoritme bebas kunci, lanjutkan. Tapi Anda masih perlu tahu apa jaminan yang akan diberikan oleh compiler Anda.
Gerhana
10

Jika Anda menginginkan kenaikan atom dalam C ++, Anda dapat menggunakan pustaka C ++ 0x ( std::atomictipe data) atau sesuatu seperti TBB.

Dahulu kala pedoman pengkodean GNU mengatakan bahwa memperbarui tipe data yang sesuai dengan satu kata adalah "biasanya aman" tetapi saran itu salah untuk mesin SMP, salah untuk beberapa arsitektur, dan salah saat menggunakan kompiler pengoptimalan.


Untuk memperjelas komentar "memperbarui tipe data satu kata":

Ada kemungkinan dua CPU pada mesin SMP untuk menulis ke lokasi memori yang sama dalam siklus yang sama, dan kemudian mencoba menyebarkan perubahan ke CPU lain dan cache. Meskipun hanya satu kata dari data yang sedang ditulis sehingga penulisan hanya membutuhkan satu siklus untuk diselesaikan, penulisan tersebut juga terjadi secara bersamaan sehingga Anda tidak dapat menjamin penulisan mana yang berhasil. Anda tidak akan mendapatkan sebagian data yang diperbarui, tetapi satu tulisan akan hilang karena tidak ada cara lain untuk menangani kasus ini.

Bandingkan-dan-tukar koordinat yang benar antara beberapa CPU, tetapi tidak ada alasan untuk percaya bahwa setiap penugasan variabel dari tipe data satu kata akan menggunakan bandingkan-dan-tukar.

Dan meskipun kompiler pengoptimalan tidak memengaruhi cara memuat / penyimpanan dikompilasi, ia dapat berubah ketika pemuatan / penyimpanan terjadi, menyebabkan masalah serius jika Anda mengharapkan pembacaan dan penulisan Anda terjadi dalam urutan yang sama seperti yang muncul di kode sumber ( penguncian yang diperiksa dua kali yang paling terkenal tidak berfungsi di vanilla C ++).

CATATAN Jawaban asli saya juga mengatakan bahwa arsitektur Intel 64 bit rusak dalam menangani data 64 bit. Itu tidak benar, jadi saya mengedit jawabannya, tetapi hasil edit saya mengklaim chip PowerPC rusak. Itu benar ketika membaca nilai langsung (yaitu, konstanta) ke dalam register (lihat dua bagian bernama "Memuat penunjuk" di bawah daftar 2 dan daftar 4). Tetapi ada instruksi untuk memuat data dari memori dalam satu siklus ( lmw), jadi saya telah menghapus bagian jawaban saya itu.

Max Lybbert
sumber
Pembacaan dan penulisan sangat mudah dilakukan pada sebagian besar CPU modern jika data Anda secara alami disejajarkan dan berukuran benar, bahkan dengan SMP dan pengoptimal kompiler. Namun ada banyak peringatan, terutama dengan mesin 64 bit, jadi mungkin merepotkan untuk memastikan data Anda memenuhi persyaratan di setiap mesin.
Dan Olson
Terima kasih telah memperbarui. Benar, membaca dan menulis adalah atom karena Anda menyatakan bahwa mereka tidak dapat setengah selesai, tetapi komentar Anda menyoroti bagaimana kami mendekati fakta ini dalam praktiknya. Sama dengan penghalang memori, mereka tidak memengaruhi sifat atomik operasi, tetapi bagaimana kita mendekatinya dalam praktik.
Dan Olson
4

Pada x86 / Windows di C / C ++, Anda tidak boleh menganggapnya aman untuk thread. Anda harus menggunakan InterlockedIncrement () dan InterlockedDecrement () jika Anda memerlukan operasi atom.

i_am_jorf
sumber
4

Jika bahasa pemrograman Anda mengatakan apa-apa tentang benang, namun berjalan pada platform multithreaded, bagaimana bisa setiap konstruksi bahasa bisa aman untuk utas?

Seperti yang ditunjukkan orang lain: Anda perlu melindungi akses multithread apa pun ke variabel dengan panggilan khusus platform.

Ada pustaka di luar sana yang mengabstraksi kekhususan platform, dan standar C ++ yang akan datang telah mengadaptasi model memorinya untuk mengatasi utas (dan dengan demikian dapat menjamin keamanan utas).

xtofl
sumber
4

Bahkan jika itu direduksi menjadi satu instruksi perakitan, menambah nilai secara langsung di memori, masih belum aman untuk thread.

Saat menambah nilai dalam memori, perangkat keras melakukan operasi "baca-ubah-tulis": ia membaca nilai dari memori, menambahnya, dan menuliskannya kembali ke memori. Perangkat keras x86 tidak memiliki cara untuk menambah memori secara langsung; RAM (dan cache) hanya dapat membaca dan menyimpan nilai, bukan memodifikasinya.

Sekarang misalkan Anda memiliki dua inti terpisah, baik di soket terpisah atau berbagi satu soket (dengan atau tanpa cache bersama). Prosesor pertama membaca nilai, dan sebelum dapat menulis kembali nilai yang diperbarui, prosesor kedua akan membacanya. Setelah kedua prosesor menulis kembali nilainya, nilainya hanya akan bertambah sekali, bukan dua kali.

Ada cara untuk menghindari masalah ini; Prosesor x86 (dan sebagian besar prosesor multi-inti yang akan Anda temukan) dapat mendeteksi jenis konflik ini di perangkat keras dan mengurutkannya, sehingga seluruh urutan baca-ubah-tulis tampak seperti atom. Namun, karena ini sangat mahal, ini hanya dilakukan ketika diminta oleh kode, pada x86 biasanya melaluiLOCK awalan. Arsitektur lain dapat melakukan ini dengan cara lain, dengan hasil yang serupa; misalnya, load-linked / store-conditional dan atomic Compare-and-swap (prosesor x86 terbaru juga memiliki yang terakhir ini).

Perhatikan bahwa menggunakan volatiletidak membantu di sini; itu hanya memberitahu compiler bahwa variabel mungkin telah dimodifikasi secara eksternal dan membaca variabel itu tidak boleh di-cache dalam register atau dioptimalkan. Itu tidak membuat kompiler menggunakan atomic primitif.

Cara terbaik adalah dengan menggunakan atomic primitif (jika compiler atau library Anda memilikinya), atau lakukan penambahan secara langsung dalam assembly (menggunakan instruksi atomic yang benar).

CesarB
sumber
2

Jangan pernah berasumsi bahwa kenaikan akan dikompilasi menjadi operasi atom. Gunakan InterlockedIncrement atau fungsi serupa apa pun yang ada di platform target Anda.

Sunting: Saya baru saja mencari pertanyaan khusus ini dan kenaikan pada X86 bersifat atom pada sistem prosesor tunggal, tetapi tidak pada sistem multiprosesor. Menggunakan awalan kunci dapat menjadikannya atomic, tetapi jauh lebih portabel hanya dengan menggunakan InterlockedIncrement.

Dan Olson
sumber
1
InterlockedIncrement () adalah fungsi Windows; semua kotak Linux saya dan mesin OS X modern berbasis x64, jadi mengatakan InterlockedIncrement () 'jauh lebih portabel' daripada kode x86 agak palsu.
Pete Kirkham
Ini jauh lebih portabel dalam arti yang sama bahwa C jauh lebih portabel daripada perakitan. Tujuannya di sini adalah untuk melindungi diri Anda dari mengandalkan perakitan yang dibuat khusus untuk prosesor tertentu. Jika sistem operasi lain menjadi perhatian Anda, maka InterlockedIncrement mudah dibungkus.
Dan Olson
2

Menurut pelajaran assembly pada x86 ini, Anda dapat menambahkan register ke lokasi memori secara atomis , sehingga kode Anda mungkin secara atomik mengeksekusi '++ i' ou 'i ++'. Tetapi seperti yang dikatakan di posting lain, C ansi tidak menerapkan atomicity ke operasi '++', jadi Anda tidak dapat memastikan apa yang akan dihasilkan oleh compiler Anda.

Selso Liberado
sumber
1

Standar 1998 C ++ tidak ada hubungannya dengan utas, meskipun standar berikutnya (karena tahun ini atau tahun depan) tidak. Oleh karena itu, Anda tidak dapat mengatakan apa pun yang cerdas tentang operasi keamanan thread tanpa mengacu pada penerapannya. Ini bukan hanya prosesor yang digunakan, tetapi kombinasi dari compiler, OS, dan model thread.

Dengan tidak adanya dokumentasi yang sebaliknya, saya tidak akan berasumsi bahwa tindakan apa pun adalah thread-safe, terutama dengan prosesor multi-core (atau sistem multi-prosesor). Saya juga tidak akan mempercayai tes, karena masalah sinkronisasi utas cenderung muncul hanya secara tidak sengaja.

Tidak ada yang aman untuk thread kecuali Anda memiliki dokumentasi yang mengatakan itu untuk sistem tertentu yang Anda gunakan.

David Thornley
sumber
1

Lemparkan saya ke dalam thread penyimpanan lokal; itu bukan atom, tapi kemudian tidak masalah.


sumber
1

AFAIK, Menurut standar C ++, baca / tulis ke intatomic.

Namun, semua yang dilakukannya adalah menyingkirkan perilaku tidak terdefinisi yang terkait dengan data race.

Tetapi masih akan ada perlombaan data jika kedua utas mencoba menambah i.

Bayangkan skenario berikut:

Mari i = 0awalnya:

Thread A membaca nilai dari memori dan menyimpannya di cache-nya sendiri. Thread A menambah nilai sebesar 1.

Thread B membaca nilai dari memori dan menyimpannya di cache-nya sendiri. Thread B menambah nilai sebesar 1.

Jika ini semua adalah satu utas, Anda akan masuk i = 2ke memori.

Namun dengan kedua utas, setiap utas menulis perubahannya dan Utas A menulis i = 1kembali ke memori, dan utas B menulis i = 1ke memori.

Ini didefinisikan dengan baik, tidak ada kerusakan sebagian atau konstruksi atau segala jenis robekan suatu objek, tetapi itu masih perlombaan data.

Untuk meningkatkan atom iAnda dapat menggunakan:

std::atomic<int>::fetch_add(1, std::memory_order_relaxed)

Pemesanan santai dapat digunakan karena kami tidak peduli di mana operasi ini berlangsung, yang kami pedulikan adalah bahwa operasi kenaikan adalah atom.

Moshe Rabaev
sumber
0

Anda mengatakan "ini hanya satu instruksi, itu tidak akan dapat diganggu oleh sakelar konteks." - itu semua baik dan bagus untuk satu CPU, tapi bagaimana dengan CPU dual core? Kemudian Anda benar-benar dapat memiliki dua utas yang mengakses variabel yang sama pada saat yang sama tanpa sakelar konteks apa pun.

Tanpa mengetahui bahasanya, jawabannya adalah mengujinya.

Chris
sumber
4
Anda tidak akan mengetahui apakah sesuatu itu aman untuk thread dengan mengujinya - masalah threading bisa menjadi satu dari sejuta kejadian. Anda mencarinya di dokumentasi Anda. Jika tidak dijamin threadsafe oleh dokumentasi Anda, itu tidak dijamin.
Gerhana
2
Setuju dengan @Josh di sini. Sesuatu hanya thread-safe jika dapat dibuktikan secara matematis melalui analisis kode yang mendasarinya. Tidak ada jumlah pengujian yang dapat mulai mendekati itu.
Rex M
Itu adalah jawaban yang bagus sampai kalimat terakhir itu.
Rob K
0

Menurut saya, jika ekspresi "i ++" adalah satu-satunya dalam pernyataan, itu setara dengan "++ i", kompilernya cukup pintar untuk tidak menyimpan nilai temporal, dll. Jadi, jika Anda dapat menggunakannya secara bergantian (jika tidak, Anda menang tidak bertanya mana yang akan digunakan), tidak masalah mana pun yang Anda gunakan karena hampir sama (kecuali untuk estetika).

Bagaimanapun, meskipun operator kenaikan adalah atom, itu tidak menjamin bahwa penghitungan lainnya akan konsisten jika Anda tidak menggunakan kunci yang benar.

Jika Anda ingin bereksperimen sendiri, tulis program di mana N utas bertambah secara bersamaan variabel bersama M kali masing-masing ... jika nilainya kurang dari N * M, maka beberapa kenaikan telah ditimpa. Cobalah dengan preincrement dan postincrement dan beri tahu kami ;-)

fortran
sumber
0

Untuk penghitung, saya merekomendasikan penggunaan idiom bandingkan dan tukar yang tidak mengunci dan aman untuk utas.

Ini dia di Jawa:

public class IntCompareAndSwap {
    private int value = 0;

    public synchronized int get(){return value;}

    public synchronized int compareAndSwap(int p_expectedValue, int p_newValue){
        int oldValue = value;

        if (oldValue == p_expectedValue)
            value = p_newValue;

        return oldValue;
    }
}

public class IntCASCounter {

    public IntCASCounter(){
        m_value = new IntCompareAndSwap();
    }

    private IntCompareAndSwap m_value;

    public int getValue(){return m_value.get();}

    public void increment(){
        int temp;
        do {
            temp = m_value.get();
        } while (temp != m_value.compareAndSwap(temp, temp + 1));

    }

    public void decrement(){
        int temp;
        do {
            temp = m_value.get();
        } while (temp > 0 && temp != m_value.compareAndSwap(temp, temp - 1));

    }
}
AtariPete
sumber
Tampak mirip dengan fungsi test_and_set.
samoz
1
Anda menulis "tidak mengunci", tetapi bukankah "tersinkronisasi" berarti mengunci?
Corey Trager