Pelajaran apa yang Anda pelajari dari proyek yang hampir / benar-benar gagal karena multithreading yang buruk? [Tutup]

11

Pelajaran apa yang Anda pelajari dari proyek yang hampir / benar-benar gagal karena multithreading yang buruk?

Kadang-kadang, kerangka kerja memaksakan model threading tertentu yang membuat urutan urutan besarnya lebih sulit untuk diperbaiki.

Bagi saya, saya belum pulih dari kegagalan terakhir dan saya merasa lebih baik saya tidak mengerjakan apa pun yang berkaitan dengan multithreading dalam kerangka itu.

Saya menemukan bahwa saya bagus dalam masalah multithreading yang memiliki fork / join sederhana, dan di mana data hanya bergerak dalam satu arah (sementara sinyal dapat melakukan perjalanan dalam arah melingkar).

Saya tidak dapat menangani GUI di mana beberapa pekerjaan hanya dapat dilakukan pada utas yang disambungkan secara serial ("utas utama") dan pekerjaan lain hanya dapat dilakukan pada utas apa pun kecuali utas utama ("utas pekerja"), dan di mana data dan pesan harus melakukan perjalanan ke semua arah antara komponen N (grafik yang terhubung penuh).

Pada saat saya meninggalkan proyek itu untuk proyek lain, ada masalah kebuntuan di mana-mana. Saya mendengar bahwa 2-3 bulan kemudian, beberapa pengembang lain berhasil memperbaiki semua masalah kebuntuan, hingga dapat dikirim ke pelanggan. Saya tidak pernah berhasil menemukan bahwa saya tidak memiliki pengetahuan yang hilang.

Sesuatu tentang proyek: jumlah ID pesan (nilai integer yang menggambarkan arti dari suatu peristiwa yang dapat dikirim ke antrian pesan objek lain, terlepas dari threading) mencapai beberapa ribu. String unik (pesan pengguna) juga mengalami sekitar seribu.

Ditambahkan

Analogi terbaik yang saya dapatkan dari tim lain (tidak terkait dengan proyek saya dulu atau sekarang) adalah "memasukkan data ke dalam basis data". ("Basis data" mengacu pada sentralisasi dan pembaruan atom.) Dalam GUI yang terpecah menjadi beberapa tampilan, semua berjalan pada "utas utama" yang sama dan semua pengangkatan berat non-GUI dilakukan dalam utas pekerja individu, data aplikasi harus disimpan dalam satu tempat yang bertindak seperti Database, dan biarkan "Database" menangani semua "pembaruan atom" yang melibatkan dependensi data yang tidak sepele. Semua bagian lain dari GUI hanya menangani gambar layar dan tidak ada yang lain. Bagian UI dapat men-cache hal-hal dan pengguna tidak akan melihat jika basi hanya sepersekian detik, jika itu dirancang dengan benar. "Database" ini juga dikenal sebagai "dokumen" dalam arsitektur Document-View. Sayangnya - tidak, aplikasi saya sebenarnya menyimpan semua data di Views. Saya tidak tahu mengapa seperti itu.

Kontributor kontributor:

(kontributor tidak perlu menggunakan contoh nyata / pribadi. Pelajaran dari contoh anekdotal, jika dinilai sendiri dapat dipercaya, juga diterima.)

rwong
sumber
Saya pikir bisa 'berpikir di utas' adalah sedikit bakat dan kurang sesuatu yang bisa dipelajari, karena kurangnya kata-kata yang lebih baik. Saya tahu banyak pengembang yang telah bekerja dengan sistem paralel untuk waktu yang sangat lama, tetapi mereka tersedak jika data harus masuk lebih dari satu arah.
dauphic

Jawaban:

13

Pelajaran favorit saya - sangat sulit dimenangkan! - adalah bahwa dalam program multithreaded, penjadwal adalah babi licik yang membenci Anda. Jika ada yang salah, mereka akan melakukannya, tetapi dengan cara yang tak terduga. Dapatkan sesuatu yang salah, dan Anda akan mengejar heisenbugs aneh (karena setiap instrument yang Anda tambahkan akan mengubah timing dan memberi Anda pola lari yang berbeda).

Satu-satunya cara yang waras untuk memperbaikinya adalah dengan benar - benar mengunci semua penanganan thread menjadi sekecil kode yang membuatnya baik-baik saja dan yang sangat konservatif tentang memastikan bahwa kunci dipegang dengan benar (dan dengan urutan akuisisi yang konstan secara global juga) . Cara termudah untuk melakukannya adalah tidak membagikan memori (atau sumber daya lainnya) di antara utas kecuali untuk olahpesan yang harus tidak sinkron; yang memungkinkan Anda menulis segala sesuatu dengan gaya yang tidak dikenali oleh utas. (Bonus: menskalakan ke beberapa mesin dalam sebuah cluster jauh lebih mudah.)

Donal Fellows
sumber
+1 untuk "tidak membagikan memori (atau sumber daya lainnya) antara utas kecuali untuk olahpesan yang harus tidak sinkron;"
Nemanja Trifunovic
1
Satu- satunya jalan? Bagaimana dengan tipe data yang tidak berubah?
Aaronaught
is that in a multithreaded program the scheduler is a sneaky swine that hates you.- tidak, itu tidak tepat seperti yang Anda suruh :)
mattnz
@Aaronaught: Nilai-nilai global yang disahkan oleh referensi, bahkan jika tidak berubah, masih membutuhkan GC global dan yang memperkenalkan kembali sejumlah besar sumber daya global. Mampu menggunakan manajemen memori per-thread itu bagus, karena memungkinkan Anda menyingkirkan sejumlah kunci global.
Donal Fellows
Bukannya Anda tidak bisa meneruskan nilai tipe non-dasar dengan referensi, tetapi itu membutuhkan tingkat penguncian yang lebih tinggi (misalnya, "pemilik" yang memegang referensi hingga beberapa pesan kembali, yang mudah untuk dikacaukan dalam pemeliharaan) atau kode kompleks di mesin pesan untuk mentransfer kepemilikan. Atau Anda menyusun semuanya dan tidak membuat di thread lain, yang jauh lebih lambat (Anda harus tetap melakukannya saat pergi ke sebuah cluster). Memotong untuk mengejar dan tidak berbagi memori sama sekali lebih mudah.
Donal Fellows
6

Berikut adalah beberapa pelajaran dasar yang dapat saya pikirkan saat ini (bukan dari proyek gagal tetapi dari masalah nyata yang terlihat pada proyek nyata):

  • Cobalah untuk menghindari pemblokiran panggilan saat memegang sumber daya bersama. Pola kebuntuan umum adalah ulir meraih mutex, membuat panggilan balik, blok panggilan balik pada mutex yang sama.
  • Lindungi akses ke struktur data apa pun yang dibagikan dengan bagian mutex / kritis (atau gunakan yang bebas kunci - tapi jangan buat sendiri!)
  • Jangan anggap atomisitas - gunakan API atomik (mis. InterlockedIncrement).
  • RTFM tentang keamanan thread perpustakaan, objek atau API yang Anda gunakan.
  • Manfaatkan primitif sinkronisasi yang tersedia, misalnya acara, semafor. (Tapi perhatikan baik-baik ketika menggunakannya bahwa Anda tahu Anda dalam keadaan baik - saya telah melihat banyak contoh peristiwa yang ditandai dalam kondisi yang salah sehingga acara atau data dapat hilang)
  • Asumsikan utas dapat dijalankan secara bersamaan dan / atau atas perintah apa pun dan konteks itu dapat beralih antar utas kapan saja (kecuali di bawah OS yang memberikan jaminan lain).
Guy Sirton
sumber
6
  • Seluruh proyek GUI Anda seharusnya hanya dipanggil dari utas utama . Pada dasarnya, Anda tidak harus memasukkan ".net" tunggal ke dalam GUI Anda. Multithreading harus terjebak dalam proyek terpisah yang menangani akses data yang lebih lambat.

Kami mewarisi bagian di mana proyek GUI menggunakan selusin utas. Ini memberi apa-apa selain masalah. Jalan buntu, masalah balap, panggilan GUI lintas benang ...

Carra
sumber
Apakah "proyek" berarti "perakitan"? Saya tidak melihat bagaimana distribusi kelas di antara majelis akan menyebabkan masalah threading.
nikie
Dalam proyek saya itu memang sebuah majelis. Tetapi intinya adalah bahwa semua kode dalam folder tersebut harus dipanggil dari utas utama, tanpa pengecualian.
Carra
Saya tidak berpikir aturan ini berlaku secara umum. Ya, Anda seharusnya tidak pernah memanggil kode GUI dari utas lainnya. Tetapi bagaimana Anda mendistribusikan kelas ke folder / proyek / majelis adalah keputusan independen.
nikie
1

Java 5 dan yang lebih baru memiliki Pelaksana yang dimaksudkan untuk membuat hidup lebih mudah untuk menangani program multi-threading fork-join style.

Gunakan itu, itu akan menghilangkan banyak rasa sakit.

(dan, ya, ini saya pelajari dari sebuah proyek :))


sumber
1
Untuk menerapkan jawaban ini ke bahasa lain - gunakan kerangka kerja pemrosesan paralel berkualitas tinggi yang disediakan oleh bahasa itu jika memungkinkan. (Namun, hanya waktu yang akan menentukan apakah kerangka kerja benar-benar hebat dan sangat bermanfaat.)
rwong
1

Saya memiliki latar belakang dalam sistem embedded realtime yang sulit. Anda tidak dapat menguji tidak adanya masalah yang disebabkan oleh multithreading. (Terkadang Anda dapat mengkonfirmasi keberadaan). Kode harus terbukti benar. Jadi praktik terbaik di sekitar setiap dan semua interaksi utas.

  • Aturan # 1: CIUMAN - Jika tidak perlu utas, jangan putar satu. Serialise sebanyak mungkin.
  • Aturan # 2: Jangan melanggar # 1.
  • # 3 Jika Anda tidak dapat membuktikan melalui ulasan itu benar, itu tidak.
mattnz
sumber
+1 untuk aturan 1. Saya sedang mengerjakan proyek yang awalnya akan diblokir sampai utas lainnya selesai - pada dasarnya panggilan metode! Untungnya, kami memutuskan menentang pendekatan itu.
Michael K
FTW # 3 Lebih baik menghabiskan berjam-jam berjuang dengan diagram penguncian waktu atau apa pun yang Anda gunakan untuk membuktikan bahwa itu baik daripada bulan bertanya-tanya mengapa kadang-kadang berantakan.
1

Analogi dari kelas multithreading yang saya ambil tahun lalu sangat membantu. Sinkronisasi ulir seperti sinyal lalu lintas yang melindungi persimpangan (data) agar tidak digunakan oleh dua mobil (utas) sekaligus. Kesalahan yang dilakukan banyak pengembang adalah menyalakan lampu merah di sebagian besar kota untuk membiarkan satu mobil lewat karena mereka pikir terlalu sulit atau berbahaya untuk mengetahui sinyal persis yang mereka butuhkan. Itu mungkin bekerja dengan baik ketika lalu lintas sepi, tetapi akan menyebabkan kemacetan saat aplikasi Anda tumbuh.

Itu adalah sesuatu yang sudah saya ketahui dalam teori, tetapi setelah kelas itu analogi itu benar-benar melekat pada saya, dan saya kagum seberapa sering setelah itu saya akan menyelidiki masalah threading dan menemukan satu antrian raksasa, atau menyela dimatikan di mana-mana selama menulis ke variabel hanya dua utas yang digunakan, atau muteks ditahan lama ketika itu bisa di refactored untuk menghindarinya sama sekali.

Dengan kata lain, beberapa masalah threading terburuk disebabkan oleh usaha keras yang berusaha menghindari masalah threading.

Karl Bielefeldt
sumber
0

Coba lakukan lagi.

Setidaknya bagi saya, apa yang menciptakan perbedaan adalah latihan. Setelah melakukan pekerjaan multi-threaded dan didistribusikan beberapa kali Anda hanya bisa menguasainya.

Saya pikir debugging benar-benar yang membuatnya sulit. Saya dapat men-debug kode multi-threaded menggunakan VS tapi saya benar-benar bingung jika saya harus menggunakan gdb. Kesalahan saya, mungkin.

Hal lain yang dipelajari lebih lanjut adalah mengunci struktur data gratis.

Saya pikir pertanyaan ini dapat benar-benar ditingkatkan jika Anda menentukan kerangka kerjanya. .NET thread pools dan pekerja latar belakang benar-benar berbeda dari QThread, misalnya. Selalu ada beberapa platform khusus Gotcha.

Vitor Py
sumber
Saya tertarik mendengar cerita dari berbagai kerangka kerja, karena saya percaya ada hal-hal yang harus dipelajari dari setiap kerangka kerja, terutama yang belum saya ketahui.
rwong
1
Debuggers sebagian besar tidak berguna di lingkungan multi-thread.
Pemdas
Saya sudah memiliki pelacak eksekusi multi-threaded yang memberi tahu saya apa masalahnya, tetapi tidak akan membantu saya menyelesaikannya. Inti dari masalah saya adalah bahwa "menurut desain saat ini, saya tidak dapat mengirimkan pesan X ke objek Y dengan cara ini (urutan); harus ditambahkan ke antrian raksasa dan pada akhirnya akan diproses, tetapi karena ini , tidak ada cara bagi pesan untuk muncul kepada pengguna pada waktu yang tepat - itu akan selalu terjadi secararonologis dan membuat pengguna sangat, sangat bingung. Anda bahkan mungkin perlu menambahkan progress bar, membatalkan tombol atau pesan kesalahan ke tempat - tempat yang seharusnya tidak ' "Aku punya itu ."
rwong
0

Saya telah belajar bahwa panggilan balik dari modul tingkat lebih rendah ke modul tingkat lebih tinggi adalah kejahatan besar karena mereka menyebabkan memperoleh kunci dalam urutan yang berlawanan.

Sergej Zagursky
sumber
panggilan balik bukanlah kejahatan ... fakta bahwa mereka melakukan sesuatu selain dari putus benang mungkin adalah akar dari kejahatan. Saya akan sangat curiga terhadap panggilan balik yang tidak hanya mengirim token ke antrian pesan.
Pemdas
Memecahkan masalah optimisasi (seperti meminimalkan f (x)) sering diimplementasikan dengan memberikan pointer ke fungsi f (x) untuk prosedur optimasi, yang "memanggilnya kembali" sambil mencari minimum. Bagaimana Anda melakukannya tanpa panggilan balik?
quant_dev
1
Tidak ada downvote, tetapi panggilan balik tidak jahat. Memanggil panggilan balik sambil memegang kunci adalah jahat. Jangan panggil apa pun di dalam kunci ketika Anda tidak tahu apakah itu bisa mengunci atau menunggu. Itu tidak hanya mencakup panggilan balik tetapi juga fungsi virtual, fungsi API, fungsi dalam modul lain ("tingkat lebih tinggi" atau "tingkat lebih rendah").
nikie
@nikie: Jika kunci harus dipegang selama panggilan balik, baik API lainnya harus dirancang untuk menjadi reentrant (keras!) atau fakta bahwa Anda memegang kunci harus menjadi bagian yang terdokumentasi dari API ( disayangkan, tetapi kadang-kadang semua bisa Anda lakukan).
Donal Fellows
@Donal Fellows: Jika kunci harus dipegang saat panggilan balik, saya katakan Anda memiliki cacat desain. Jika benar-benar tidak ada cara lain, maka ya, tentu saja dokumentasikan saja! Sama seperti Anda akan mendokumentasikan jika panggilan balik akan dipanggil di utas latar belakang. Itu bagian dari antarmuka.
nikie