Bagaimana keamanan thread dapat disediakan oleh bahasa pemrograman yang mirip dengan cara keamanan memori disediakan oleh Java dan C #?

10

Java dan C # memberikan keamanan memori dengan memeriksa batas array dan pointer dereferences.

Mekanisme apa yang dapat diimplementasikan ke dalam bahasa pemrograman untuk mencegah kemungkinan kondisi balapan dan kebuntuan?

mrpyo
sumber
3
Anda mungkin tertarik dengan apa yang dilakukan Rust: Fearless Concurrency dengan Rust
Vincent Savard
2
Jadikan semuanya tidak berubah, atau buat semuanya asinkron dengan saluran aman. Anda mungkin juga tertarik dengan Go dan Erlang .
Theraot
@Theraot "buat semuanya async dengan saluran aman" - andai Anda bisa menguraikannya.
mrpyo
2
@ mrpyo Anda tidak akan mengekspos proses atau utas, setiap panggilan adalah janji, semuanya berjalan bersamaan (dengan runtime menjadwalkan eksekusi mereka, dan membuat / menyatukan benang sistem di belakang layar sesuai kebutuhan), dan logika yang melindungi negara ada dalam mekanisme yang meneruskan informasi sekitar ... runtime dapat secara otomatis bersambung dengan penjadwalan, dan akan ada perpustakaan standar dengan solusi aman untuk perilaku yang lebih bernuansa, khususnya produsen / konsumen dan agregasi diperlukan.
Theraot
2
Omong-omong, ada pendekatan lain yang mungkin: memori transaksional .
Theraot

Jawaban:

14

Ras terjadi ketika Anda memiliki alias secara simultan dari suatu objek dan, setidaknya, salah satu alias bermutasi.

Jadi, untuk mencegah balapan, Anda harus membuat satu atau lebih kondisi ini tidak benar.

Berbagai pendekatan menangani berbagai aspek. Pemrograman fungsional menekankan keabadian yang menghilangkan mutabilitas. Mengunci / atom menghapus simultanitas. Jenis affine menghapus aliasing (Rust menghapus aliasing yang bisa berubah). Model aktor biasanya menghapus alias.

Anda dapat membatasi objek yang dapat alias agar lebih mudah untuk memastikan bahwa kondisi di atas dihindari. Di situlah saluran dan / atau gaya penyampaian pesan masuk. Anda tidak dapat alias memori sewenang-wenang, hanya akhir dari saluran atau antrian yang diatur untuk bebas ras. Biasanya dengan menghindari simultan yaitu kunci atau atom.

Kelemahan dari berbagai mekanisme ini adalah mereka membatasi program yang dapat Anda tulis. Semakin tumpul batasannya, semakin sedikit programnya. Jadi tidak ada aliasing atau tidak ada mutabilitas yang bekerja, dan mudah dipikirkan, tetapi sangat terbatas.

Itu sebabnya Rust menyebabkan kehebohan seperti itu. Ini adalah bahasa teknik (tidak seperti yang akademis) yang mendukung aliasing dan kemampuan berubah tetapi memiliki kompiler memeriksa bahwa mereka tidak terjadi secara bersamaan. Meskipun bukan yang ideal, itu memungkinkan kelas yang lebih besar dari program yang ditulis dengan aman daripada banyak pendahulunya.

Alex
sumber
11

Java dan C # memberikan keamanan memori dengan memeriksa batas array dan pointer dereferences.

Sangat penting untuk terlebih dahulu memikirkan bagaimana C # dan Java melakukan ini. Mereka melakukannya dengan mengubah perilaku yang tidak terdefinisi dalam C atau C ++ menjadi perilaku yang didefinisikan: crash program . Pengecualian null dan indeks array tidak boleh ditangkap dalam program C # atau Java yang benar; mereka seharusnya tidak terjadi sejak awal karena program seharusnya tidak memiliki bug itu.

Tapi saya pikir bukan itu yang Anda maksud dengan pertanyaan Anda! Kita bisa dengan mudah menulis runtime "deadlock safe" yang secara berkala memeriksa untuk melihat apakah ada n thread yang saling menunggu satu sama lain dan mengakhiri program jika itu terjadi, tapi saya tidak berpikir itu akan memuaskan Anda.

Mekanisme apa yang dapat diimplementasikan ke dalam bahasa pemrograman untuk mencegah kemungkinan kondisi balapan dan kebuntuan?

Masalah berikutnya yang kita hadapi dengan pertanyaan Anda adalah bahwa "kondisi ras", tidak seperti deadlock, sulit dideteksi. Ingat, apa yang kita kejar dalam keselamatan benang bukanlah menghilangkan ras . Yang kami kejar adalah membuat program ini benar, tidak peduli siapa yang memenangkan lomba ! Masalah dengan kondisi balapan bukanlah bahwa dua utas berjalan dalam urutan yang tidak ditentukan dan kita tidak tahu siapa yang akan menyelesaikan terlebih dahulu. Masalah dengan kondisi balapan adalah bahwa pengembang lupa bahwa beberapa pesanan penyelesaian ulah dimungkinkan dan gagal memperhitungkan kemungkinan itu.

Jadi pertanyaan Anda pada dasarnya bermuara pada "adakah cara agar bahasa pemrograman dapat memastikan bahwa program saya benar?" dan jawaban untuk pertanyaan itu adalah, dalam praktiknya, tidak.

Sejauh ini saya hanya mengkritik pertanyaan Anda. Biarkan saya mencoba untuk pindah persneling di sini dan mengatasi semangat pertanyaan Anda. Adakah pilihan yang bisa dibuat oleh perancang bahasa yang akan mengurangi situasi mengerikan yang kita alami dengan multithreading?

Situasinya benar-benar mengerikan! Mendapatkan kode multithreaded dengan benar, terutama pada arsitektur model memori yang lemah, sangat, sangat sulit. Penting untuk memikirkan mengapa itu sulit:

  • Beberapa utas kontrol dalam satu proses sulit dipikirkan. Satu utas cukup sulit!
  • Abstraksi menjadi sangat bocor di dunia multithreaded. Di dunia utas tunggal, kami dijamin bahwa program berperilaku seolah-olah dijalankan secara berurutan, meskipun tidak benar-benar berjalan berurutan. Di dunia multithreaded, itu tidak lagi terjadi; optimasi yang tidak akan terlihat pada satu utas menjadi terlihat, dan sekarang pengembang perlu memahami kemungkinan optimasi tersebut.
  • Tapi itu semakin buruk. Spesifikasi C # mengatakan bahwa suatu implementasi TIDAK diperlukan untuk memiliki urutan baca dan tulis yang konsisten yang dapat disepakati oleh semua utas . Gagasan bahwa ada "ras" sama sekali, dan bahwa ada pemenang yang jelas, sebenarnya tidak benar! Pertimbangkan situasi di mana ada dua tulisan dan dua terbaca ke beberapa variabel di banyak utas. Dalam dunia yang masuk akal kita mungkin berpikir "yah, kita tidak bisa tahu siapa yang akan memenangkan perlombaan, tetapi setidaknya akan ada balapan dan seseorang akan menang". Kita tidak berada di dunia yang masuk akal itu. C # memungkinkan beberapa utas untuk tidak setuju tentang urutan di mana membaca dan menulis terjadi; tidak ada dunia yang konsisten yang diamati semua orang.

Jadi ada cara yang jelas bahwa perancang bahasa dapat membuat segalanya lebih baik. Abaikan kemenangan kinerja prosesor modern . Buat semua program, bahkan yang multi-utas, memiliki model memori yang sangat kuat. Ini akan membuat program multithread banyak, berkali-kali lebih lambat, yang bekerja secara langsung dengan alasan memiliki program multithreaded di tempat pertama: untuk peningkatan kinerja.

Meskipun mengesampingkan model memori, ada alasan lain mengapa multithreading sulit:

  • Mencegah kebuntuan membutuhkan analisis seluruh program; Anda perlu mengetahui tatanan global di mana kunci dapat diambil, dan menegakkan tatanan itu di seluruh program, bahkan jika program terdiri dari komponen yang ditulis pada waktu yang berbeda oleh organisasi yang berbeda.
  • Alat utama yang kami berikan kepada Anda untuk menjinakkan multithreading adalah kunci, tetapi kunci tidak dapat dibuat .

Poin terakhir itu memberikan penjelasan lebih lanjut. Yang dimaksud dengan "composable" adalah:

Misalkan kita ingin menghitung int yang diberi double. Kami menulis implementasi perhitungan yang benar:

int F(double x) { correct implementation here }

Misalkan kita ingin menghitung string yang diberikan int:

string G(int y) { correct implementation here }

Sekarang jika kita ingin menghitung string yang diberi ganda:

double d = whatever;
string r = G(F(d));

G dan F dapat disusun menjadi solusi yang tepat untuk masalah yang lebih kompleks.

Tetapi kunci tidak memiliki properti ini karena kebuntuan. Metode yang benar M1 yang mengambil kunci dalam urutan L1, L2, dan metode yang benar M2 yang mengambil kunci dalam urutan L2, L1, tidak bisa keduanya digunakan dalam program yang sama tanpa membuat program yang salah. Kunci membuatnya sehingga Anda tidak bisa mengatakan "setiap metode individu benar, sehingga semuanya benar".

Jadi, apa yang bisa kita lakukan, sebagai perancang bahasa?

Pertama, jangan ke sana. Beberapa utas kontrol dalam satu program adalah ide yang buruk, dan berbagi memori di utas adalah ide yang buruk, jadi jangan memasukkannya ke dalam bahasa atau runtime.

Ini tampaknya bukan pemula.

Mari kita mengalihkan perhatian kita ke pertanyaan yang lebih mendasar: mengapa kita memiliki banyak utas? Ada dua alasan utama, dan mereka sering digabungkan ke dalam hal yang sama, meskipun mereka sangat berbeda. Mereka menyatu karena mereka berdua tentang mengelola latensi.

  • Kami salah membuat utas untuk mengelola latensi IO. Perlu menulis file besar, mengakses basis data jarak jauh, apa pun, membuat utas pekerja alih-alih mengunci utas UI Anda.

Ide buruk. Sebagai gantinya, gunakan asynchrony berulir tunggal melalui coroutine. C # melakukan ini dengan indah. Jawa, tidak begitu baik. Tapi ini adalah cara utama bahwa tanaman perancang bahasa saat ini membantu menyelesaikan masalah threading. The awaitoperator dalam C # (terinspirasi oleh F # alur kerja asynchronous dan prior art lainnya) sedang dimasukkan ke dalam semakin banyak bahasa.

  • Kami membuat utas, dengan benar, untuk menjenuhkan CPU yang menganggur dengan kerja berat secara komputasi. Pada dasarnya, kami menggunakan utas sebagai proses yang ringan.

Desainer bahasa dapat membantu dengan menciptakan fitur bahasa yang bekerja dengan baik dengan paralelisme. Pikirkan tentang bagaimana LINQ diperluas secara alami ke PLINQ, misalnya. Jika Anda orang yang bijaksana dan membatasi operasi TPL Anda untuk operasi yang terikat CPU yang sangat paralel dan tidak berbagi memori, Anda bisa mendapatkan kemenangan besar di sini.

apa lagi yang bisa kita lakukan?

  • Buat kompiler mendeteksi kesalahan yang paling bodoh dan mengubahnya menjadi peringatan atau kesalahan.

C # tidak memungkinkan Anda untuk menunggu di kunci, karena itu adalah resep untuk kebuntuan. C # tidak memungkinkan Anda untuk mengunci tipe nilai karena itu selalu hal yang salah untuk dilakukan; Anda mengunci kotak, bukan nilainya. C # memperingatkan Anda jika Anda alias volatile, karena alias tidak memaksakan mengakuisisi / melepaskan semantik. Ada banyak cara kompiler dapat mendeteksi masalah umum dan mencegahnya.

  • Desain fitur "lubang kualitas", di mana cara paling alami untuk melakukannya juga merupakan cara yang paling benar.

C # dan Java membuat kesalahan desain yang sangat besar dengan memungkinkan Anda menggunakan objek referensi apa pun sebagai monitor. Itu mendorong semua jenis praktik buruk yang membuatnya lebih sulit untuk melacak kebuntuan dan lebih sulit untuk mencegahnya secara statis. Dan itu membuang byte di setiap header objek. Monitor harus diminta berasal dari kelas monitor.

  • Sejumlah besar waktu dan upaya Microsoft Research berupaya untuk menambahkan memori transaksional perangkat lunak ke bahasa mirip C #, dan mereka tidak pernah melakukannya dengan cukup baik untuk memasukkannya ke dalam bahasa utama.

STM adalah ide yang indah, dan saya telah bermain-main dengan implementasi mainan di Haskell; ini memungkinkan Anda untuk menyusun solusi yang benar secara lebih elegan dari bagian yang benar daripada solusi berbasis kunci. Namun, saya tidak cukup tahu tentang perincian untuk mengatakan mengapa itu tidak dapat dibuat untuk bekerja pada skala; tanya Joe Duffy lain kali Anda melihatnya.

  • Jawaban lain sudah menyebutkan ketidakberdayaan. Jika Anda memiliki kekekalan dikombinasikan dengan coroutine yang efisien maka Anda dapat membangun fitur seperti model aktor langsung ke dalam bahasa Anda; Pikirkan Erlang, misalnya.

Ada banyak penelitian dalam proses bahasa berbasis kalkulus dan saya tidak mengerti ruang itu dengan sangat baik; coba baca beberapa makalah tentang itu sendiri dan lihat apakah Anda mendapatkan wawasan.

  • Permudah pihak ketiga untuk menulis analisa yang baik

Setelah saya bekerja di Microsoft pada Roslyn saya bekerja di Coverity, dan salah satu hal yang saya lakukan adalah mendapatkan alat analisis menggunakan Roslyn. Dengan memiliki analisis leksikal, sintaksis, dan semantik yang akurat yang disediakan oleh Microsoft, kami kemudian dapat berkonsentrasi pada kerja keras penulisan detektor yang menemukan masalah umum multithreading.

  • Naikkan tingkat abstraksi

Alasan mendasar mengapa kita memiliki ras dan kebuntuan dan semua itu adalah karena kita sedang menulis program yang mengatakan apa yang harus dilakukan , dan ternyata kita semua omong kosong dalam menulis program penting; komputer melakukan apa yang Anda katakan, dan kami memerintahkannya untuk melakukan hal yang salah. Banyak bahasa pemrograman modern lebih dan lebih banyak tentang pemrograman deklaratif: katakan apa hasil yang Anda inginkan, dan biarkan kompiler mencari cara yang efisien, aman, benar untuk mencapai hasil itu. Sekali lagi, pikirkan LINQ; kami ingin Anda mengatakan from c in customers select c.FirstName, yang mengekspresikan niat . Biarkan kompiler mengetahui cara menulis kode.

  • Gunakan komputer untuk memecahkan masalah komputer.

Algoritma pembelajaran mesin jauh lebih baik dalam beberapa tugas daripada algoritma kode tangan, meskipun tentu saja ada banyak pengorbanan termasuk kebenaran, waktu yang dibutuhkan untuk melatih, bias yang diperkenalkan oleh pelatihan yang buruk, dan sebagainya. Tetapi sangat mungkin bahwa banyak sekali tugas yang saat ini kami kode "dengan tangan" akan segera menerima solusi yang dihasilkan mesin. Jika manusia tidak menulis kode, mereka tidak menulis bug.

Maaf itu agak mengoceh di sana; ini adalah topik besar dan sulit dan tidak ada konsensus yang jelas telah muncul di komunitas PL dalam 20 tahun saya telah mengikuti kemajuan dalam ruang masalah ini.

Eric Lippert
sumber
"Jadi pertanyaan Anda pada dasarnya bermuara pada" apakah ada cara agar bahasa pemrograman dapat memastikan bahwa program saya benar? "Dan dalam praktiknya, jawaban untuk pertanyaan itu adalah tidak." - sebenarnya, sangat mungkin - ini disebut verifikasi formal, dan walaupun tidak nyaman, saya cukup yakin ini dilakukan secara rutin pada perangkat lunak penting, jadi saya tidak akan menyebutnya tidak praktis. Tapi Anda menjadi perancang bahasa mungkin tahu ini ...
mrpyo
6
@ mrpyo: Saya sangat sadar. Ada banyak masalah. Pertama: Saya pernah menghadiri konferensi verifikasi formal di mana tim peneliti MSFT mempresentasikan hasil baru yang menarik: mereka dapat memperluas teknik mereka untuk memverifikasi program multithreaded hingga dua puluh baris panjangnya dan memiliki verifikasi berjalan dalam waktu kurang dari seminggu. Ini adalah presentasi yang menarik, tetapi tidak berguna bagi saya; Saya memiliki program 20 juta garis untuk dianalisis.
Eric Lippert
@ Mrpyo: Kedua, seperti yang saya sebutkan, masalah besar dengan kunci adalah bahwa program yang terbuat dari metode thread aman tidak harus program thread aman. Memverifikasi metode individual secara formal tidak selalu membantu, dan seluruh analisis program sulit untuk program non-sepele.
Eric Lippert
6
@ mrpyo: Ketiga, masalah besar dengan analisis formal adalah apa, yang pada dasarnya kita lakukan? Kami menyajikan spesifikasi prasyarat dan postkondisi dan kemudian memverifikasi bahwa program memenuhi spesifikasi itu. Bagus; dalam teori yang benar-benar bisa dilakukan. Bahasa apa spesifikasi yang ditulis? Jika ada bahasa spesifikasi yang jelas dan dapat diverifikasi maka mari kita tulis semua program kami dalam bahasa itu , dan kompilasi itu . Kenapa kita tidak melakukan ini? Karena ternyata sangat sulit untuk menulis program yang benar dalam bahasa spek juga!
Eric Lippert
2
Menganalisis aplikasi untuk kebenaran menggunakan pra-kondisi / pasca-kondisi dimungkinkan (misalnya, menggunakan Kontrak Pengkodean). Namun demikian, analisis semacam itu hanya layak dengan syarat bahwa kondisinya dapat dikomposisikan, yang tidak terkunci. Saya juga akan mencatat bahwa menulis sebuah program dengan cara yang memungkinkan untuk analisis membutuhkan disiplin yang cermat. Misalnya, aplikasi yang gagal mematuhi Prinsip Substitusi Liskov cenderung menolak analisis.
Brian