Di tim baru saya yang saya kelola, sebagian besar kode kami adalah platform, soket TCP, dan kode jaringan http. Semua C ++. Sebagian besar berasal dari pengembang lain yang telah meninggalkan tim. Pengembang saat ini di tim sangat cerdas, tetapi sebagian besar junior dalam hal pengalaman.
Masalah terbesar kami: bug konkurensi multi-threaded. Sebagian besar pustaka kelas kami ditulis untuk tidak sinkron dengan menggunakan beberapa kelas kumpulan utas. Metode di perpustakaan kelas sering enqueue taks berjalan lama ke kumpulan thread dari satu utas dan kemudian metode panggilan balik kelas yang dipanggil pada thread yang berbeda. Akibatnya, kami memiliki banyak bug kasus tepi yang melibatkan asumsi threading yang salah. Ini menghasilkan bug halus yang melampaui hanya memiliki bagian penting dan kunci untuk menjaga terhadap masalah konkurensi.
Apa yang membuat masalah ini lebih sulit adalah bahwa upaya untuk memperbaikinya seringkali salah. Beberapa kesalahan yang saya amati tim berusaha (atau dalam kode warisan itu sendiri) mencakup sesuatu seperti berikut:
Kesalahan umum # 1 - Memperbaiki masalah konkurensi dengan hanya mengunci data yang dibagikan, tetapi lupa tentang apa yang terjadi ketika metode tidak dipanggil dalam urutan yang diharapkan. Berikut ini contoh yang sangat sederhana:
void Foo::OnHttpRequestComplete(statuscode status)
{
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
Jadi sekarang kita memiliki bug di mana Shutdown bisa dipanggil saat OnHttpNetworkRequestComplete terjadi. Penguji menemukan bug, menangkap dump crash, dan memberikan bug kepada pengembang. Dia pada gilirannya memperbaiki bug seperti ini.
void Foo::OnHttpRequestComplete(statuscode status)
{
AutoLock lock(m_cs);
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
AutoLock lock(m_cs);
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
Perbaikan di atas terlihat bagus sampai Anda menyadari ada kasing tepi yang bahkan lebih halus. Apa yang terjadi jika Shutdown dipanggil sebelum OnHttpRequestComplete dipanggil kembali? Contoh dunia nyata yang dimiliki tim saya bahkan lebih kompleks, dan kasus tepi lebih sulit ditemukan selama proses peninjauan kode.
Kesalahan Umum # 2 - memperbaiki masalah jalan buntu dengan keluar dari kunci secara membabi buta, tunggu utas lainnya selesai, lalu masukkan kembali kunci - tetapi tanpa menangani case bahwa objek baru saja diperbarui oleh utas lainnya!
Kesalahan Umum # 3 - Meskipun objek dihitung, urutan shutdown "melepaskan" pointernya. Tetapi lupa untuk menunggu utas yang masih berjalan untuk melepaskan contoh itu. Dengan demikian, komponen dimatikan dengan bersih, kemudian panggilan balik palsu atau terlambat dipanggil pada objek dalam keadaan tidak mengharapkan panggilan lagi.
Ada kasus tepi lainnya, tetapi intinya adalah ini:
Pemrograman multithreaded sangat sulit, bahkan untuk orang pintar.
Saat saya mengetahui kesalahan ini, saya menghabiskan waktu untuk mendiskusikan kesalahan dengan masing-masing pengembang untuk mengembangkan perbaikan yang lebih tepat. Tetapi saya curiga mereka sering bingung tentang bagaimana menyelesaikan setiap masalah karena banyaknya kode warisan yang harus diperbaiki oleh "kanan".
Kami akan segera dikirim, dan saya yakin tambalan yang kami terapkan akan berlaku untuk rilis yang akan datang. Setelah itu, kita akan punya waktu untuk meningkatkan basis kode dan refactor jika diperlukan. Kami tidak akan punya waktu untuk menulis ulang semuanya. Dan sebagian besar kode tidak terlalu buruk. Tapi saya mencari kode refactor sehingga masalah threading dapat dihindari sama sekali.
Satu pendekatan yang saya pertimbangkan adalah ini. Untuk setiap fitur platform yang signifikan, miliki utas tunggal khusus tempat semua acara dan panggilan balik jaringan dilakukan. Mirip dengan COM apartemen threading di Windows dengan menggunakan loop pesan. Operasi pemblokiran panjang masih bisa dikirim ke utas pool kerja, tetapi panggilan balik penyelesaian dipanggil pada utas komponen. Komponen bahkan dapat berbagi utas yang sama. Kemudian semua perpustakaan kelas yang berjalan di dalam utas dapat ditulis dengan asumsi satu dunia utas.
Sebelum saya menyusuri jalan itu, saya juga sangat tertarik jika ada teknik standar lain atau pola desain untuk menangani masalah multithreaded. Dan saya harus menekankan - sesuatu di luar sebuah buku yang menjelaskan dasar-dasar mutex dan semaphore. Apa yang kamu pikirkan?
Saya juga tertarik pada pendekatan lain untuk menuju proses refactoring. Termasuk salah satu dari yang berikut:
Sastra atau kertas tentang pola desain di sekitar utas. Sesuatu di luar pengantar tentang mutex dan semaphore. Kita tidak perlu paralelisme masif juga, hanya cara untuk merancang model objek untuk menangani peristiwa asinkron dari utas lainnya dengan benar .
Cara membuat diagram threading dari berbagai komponen, sehingga akan mudah dipelajari dan dikembangkan solusinya. (Yaitu, setara UML untuk membahas utas di seluruh objek dan kelas)
Mendidik tim pengembangan Anda tentang masalah dengan kode multithreaded.
Apa yang akan kamu lakukan?
sumber
Jawaban:
Kode Anda memiliki masalah penting lainnya selain dari itu. Menghapus pointer secara manual? Memanggil
cleanup
fungsi? Owch. Juga, sebagaimana ditunjukkan secara akurat dalam komentar pertanyaan, Anda tidak menggunakan RAII untuk kunci Anda, yang merupakan kegagalan lain yang cukup epik dan menjamin bahwa ketikaDoSomethingImportant
melempar pengecualian, hal-hal buruk terjadi.Fakta bahwa bug multithreaded ini terjadi hanyalah gejala dari masalah inti - kode Anda memiliki semantik yang sangat buruk dalam setiap situasi threading dan Anda menggunakan alat yang sama sekali tidak dapat diandalkan dan mantan idiom. Jika saya jadi Anda, saya akan kagum karena berfungsi dengan satu utas, apalagi lebih.
Inti dari penghitungan referensi adalah bahwa utas telah merilis instance itu . Karena jika tidak, maka tidak dapat dihancurkan karena utas masih memiliki referensi.
Gunakan
std::shared_ptr
. Ketika semua benang telah dirilis (dan tidak ada , oleh karena itu, dapat memanggil fungsi, karena mereka tidak memiliki pointer untuk itu), maka destructor disebut. Ini dijamin aman.Kedua, gunakan pustaka threading sungguhan, seperti Intel Building Thread atau Pustaka Pola Paralel Microsoft. Menulis sendiri itu memakan waktu dan tidak dapat diandalkan dan kode Anda penuh dengan rincian threading yang tidak perlu. Melakukan kunci sendiri sama buruknya dengan melakukan manajemen memori Anda sendiri. Mereka telah menerapkan banyak idiom threading tujuan umum yang sangat berguna yang berfungsi dengan benar untuk Anda gunakan.
sumber
Poster-poster lain telah berkomentar dengan baik tentang apa yang harus dilakukan untuk memperbaiki masalah inti. Posting ini berkaitan dengan masalah yang lebih mendesak yaitu menambal kode lama dengan cukup baik untuk memberi Anda waktu untuk mengulang semuanya dengan cara yang benar. Dengan kata lain, ini bukan cara yang tepat untuk melakukan sesuatu, itu hanya cara untuk tertatih-tatih untuk saat ini.
Ide Anda untuk mengkonsolidasikan acara-acara utama adalah awal yang baik. Saya akan bertindak sejauh menggunakan utas pengiriman tunggal untuk menangani semua peristiwa sinkronisasi utama, di mana pun ada ketergantungan pesanan. Siapkan antrian pesan aman dan di mana pun Anda saat ini melakukan operasi sensitif konkurensi (alokasi, pembersihan, panggil balik, dll.), Alih-alih kirim pesan ke utas itu dan minta ia melakukan atau memicu operasi. Idenya adalah bahwa utas yang satu ini mengontrol semua mulai, berhenti, alokasi, dan pembersihan unit kerja.
Utas pengiriman tidak menyelesaikan masalah yang Anda uraikan, itu hanya mengkonsolidasikannya di satu tempat. Anda masih harus khawatir tentang peristiwa / pesan yang terjadi dalam urutan yang tidak terduga. Acara dengan run-time yang signifikan masih perlu dikirim ke utas lainnya, sehingga masih ada masalah dengan konkurensi pada data bersama. Salah satu cara untuk mengurangi hal itu adalah dengan menghindari melewatkan data dengan referensi. Jika memungkinkan, data dalam pesan pengiriman harus berupa salinan yang akan dimiliki oleh penerima. (Ini sejalan dengan pembuatan data yang tidak dapat diubah yang disebutkan orang lain.)
Keuntungan dari pendekatan pengiriman ini adalah bahwa dalam utas pengiriman Anda memiliki semacam safe-haven di mana Anda setidaknya tahu bahwa operasi tertentu sedang terjadi secara berurutan. Kerugiannya adalah ia menciptakan bottleneck dan overhead CPU tambahan. Saya sarankan jangan khawatir tentang salah satu dari hal-hal itu pada awalnya: fokus pada mendapatkan beberapa ukuran operasi yang benar terlebih dahulu dengan memindahkan sebanyak yang Anda bisa ke thread pengiriman. Kemudian lakukan beberapa profiling untuk melihat apa yang paling banyak menghabiskan waktu CPU dan mulai memindahkannya kembali dari utas pengiriman menggunakan teknik multithreading yang benar.
Sekali lagi, apa yang saya jelaskan bukanlah cara yang tepat untuk melakukan sesuatu, tetapi ini adalah proses yang dapat menggerakkan Anda ke arah yang benar dalam peningkatan yang cukup kecil untuk memenuhi tenggat waktu komersial.
sumber
Berdasarkan kode yang ditampilkan, Anda memiliki tumpukan WTF. Sangat sulit jika bukan tidak mungkin untuk secara bertahap memperbaiki aplikasi multi-utas yang ditulis dengan buruk. Beri tahu pemiliknya bahwa aplikasi tidak akan dapat diandalkan tanpa pengerjaan ulang yang signifikan. Beri mereka perkiraan berdasarkan inspeksi dan pengerjaan ulang setiap bit kode yang berinteraksi dengan objek bersama. Pertama beri mereka perkiraan untuk inspeksi. Kemudian Anda bisa memberikan perkiraan untuk pengerjaan ulang.
Ketika Anda mengerjakan ulang kode, Anda harus merencanakan untuk menulis kode sehingga terbukti benar. Jika Anda tidak tahu bagaimana cara melakukannya, temukan seseorang yang melakukannya, atau Anda akan berakhir di tempat yang sama.
sumber
Jika Anda memiliki waktu untuk mendedikasikan untuk refactoring aplikasi Anda, saya akan menyarankan Anda untuk melihat model aktor (lihat misalnya Theron , Casablanca , libcppa , CAF untuk implementasi C ++).
Aktor adalah objek yang berjalan secara bersamaan dan berkomunikasi satu sama lain hanya menggunakan pertukaran pesan asinkron. Jadi, semua masalah manajemen thread, mutex, deadlock, dll, ditangani oleh perpustakaan implementasi aktor dan Anda dapat berkonsentrasi pada penerapan perilaku objek Anda (aktor), yang bermuara untuk mengulangi loop
Satu pendekatan untuk Anda adalah dengan membaca topik terlebih dahulu, dan mungkin melihat satu atau dua perpustakaan untuk melihat apakah model aktor dapat diintegrasikan dalam kode Anda.
Saya telah menggunakan (versi sederhana) model ini dalam proyek saya selama beberapa bulan sekarang dan saya kagum dengan betapa kuatnya itu.
sumber
Kesalahan di sini bukanlah "lupa", tetapi "tidak memperbaikinya". Jika Anda memiliki hal-hal yang terjadi dalam urutan yang tidak terduga, Anda memiliki masalah. Anda harus menyelesaikannya alih-alih mencoba mengatasinya (menampar kunci pada sesuatu biasanya merupakan penyelesaian).
Anda harus mencoba untuk menyesuaikan model aktor / pengiriman pesan ke tingkat tertentu dan memiliki pemisahan perhatian. Peran
Foo
jelas untuk menangani semacam komunikasi HTTP. Jika Anda ingin merancang sistem Anda untuk melakukan hal ini secara paralel, itu adalah lapisan di atas yang harus menangani siklus objek dan akses sinkronisasi yang sesuai.Mencoba agar sejumlah utas beroperasi pada data yang bisa berubah yang sama itu sulit. Tapi itu juga jarang diperlukan. Semua kasus umum yang menuntut ini, telah diabstraksikan menjadi konsep yang lebih mudah dikelola dan diimplementasikan beberapa kali untuk setiap bahasa imperatif utama. Anda hanya perlu menggunakannya.
sumber
Masalah Anda cukup buruk, tetapi tipikal penggunaan C ++ yang buruk. Ulasan kode akan memperbaiki beberapa masalah ini. 30 menit, satu set bola mata menghasilkan 90% dari hasil. (Kutipan untuk ini dapat digunakan di google)
# 1 Masalah Anda harus memastikan ada hierarki kunci yang ketat untuk mencegah kebuntuan penguncian Anda.
Jika Anda mengganti Kunci Otomatis dengan pembungkus dan makro Anda bisa melakukan ini.
Simpan peta global kunci statis yang dibuat di belakang bungkus Anda. Anda menggunakan makro untuk memasukkan informasi nama file dan nomor baris ke konstruktor pembungkus kunci otomatis.
Anda juga memerlukan grafik dominator statis.
Sekarang di dalam kunci Anda harus memperbarui grafik dominator, dan jika Anda mendapatkan perubahan pemesanan Anda menyatakan kesalahan dan batalkan.
Setelah pengujian ekstensif, Anda dapat menyingkirkan sebagian besar deadlock laten.
Kode dibiarkan sebagai latihan untuk siswa.
Masalah # 2 kemudian akan hilang (kebanyakan)
Solusi aktual arsip Anda akan berfungsi. Saya telah menggunakannya sebelumnya dalam sistem misi dan kehidupan. Pandangan saya tentang ini adalah ini
Jangan berbagi data melalui variabel publik atau getter.
Peristiwa eksternal masuk melalui pengiriman multithreaded ke antrian dilayani oleh satu utas. Sekarang Anda dapat menyortir alasan tentang penanganan Acara.
Perubahan data yang melintasi benang menjadi antrian yang aman, ditangani oleh satu utas. Buat langganan. Sekarang Anda dapat mengurutkan alasan tentang aliran data.
Jika data Anda perlu lintas kota, publikasikan ke antrian data. Itu akan menyalinnya dan meneruskannya kepada pelanggan secara serempak. Juga istirahat semua ketergantungan data dalam program.
Ini cukup banyak model aktor dengan harga murah. Tautan Giorgio akan membantu.
Akhirnya, masalah Anda dengan objek mati.
Ketika Anda menghitung referensi, Anda telah memecahkan 50%. 50% lainnya adalah untuk referensi jumlah panggilan balik. Berikan referensi kepada pemegang callback. Panggilan shutdown kemudian harus menunggu nol hitungan pada refcount. Tidak menyelesaikan grafik objek yang rumit; itu masuk ke pengumpulan sampah nyata. (Yang merupakan motivasi di Jawa untuk tidak membuat janji tentang kapan atau jika selesai () akan dipanggil; untuk membuat Anda keluar dari pemrograman seperti itu.)
sumber
Untuk penjelajah masa depan: untuk melengkapi jawaban tentang model aktor, saya ingin menambahkan CSP ( mengkomunikasikan proses sekuensial ), dengan anggukan pada keluarga besar kalkulus proses. CSP mirip dengan model aktor, tetapi berpisah secara berbeda. Anda masih memiliki banyak utas, tetapi mereka berkomunikasi melalui saluran tertentu, bukan secara khusus satu sama lain, dan kedua proses harus siap untuk masing-masing mengirim dan menerima sebelum keduanya terjadi. Ada juga bahasa formal untuk membuktikan kode CSP yang benar. Saya masih beralih ke menggunakan CSP banyak, tapi saya sudah menggunakannya dalam beberapa proyek selama beberapa bulan, sekarang, dan ini sangat disederhanakan.
University of Kent memiliki implementasi C ++ ( https://www.cs.kent.ac.uk/projects/ofa/c++csp/ , dikloning di https://github.com/themasterchef/cppcsp2 ).
sumber
Saya saat ini membaca ini dan menjelaskan semua masalah yang Anda dapatkan dan bagaimana cara menghindarinya, dalam C ++ (menggunakan pustaka threading baru tapi saya pikir penjelasan global berlaku untuk kasus Anda): http: //www.amazon. com / C-Concurrency-Action-Practical-Multithreading / dp / 1933988770 / ref = sr_1_1? yaitu = UTF8 & qid = 1337934534 & sr = 8-1
Saya pribadi menggunakan UML yang disederhanakan dan hanya berasumsi bahwa pesan dilakukan secara tidak sinkron. Juga, ini benar antara "modul" tetapi di dalam modul saya tidak ingin harus tahu.
Buku ini akan membantu, tetapi saya pikir latihan / prototyping dan mentor yang berpengalaman akan lebih baik.
Saya benar-benar akan menghindari orang yang tidak memahami masalah konkurensi yang bekerja di proyek. Tapi saya kira Anda tidak bisa melakukan itu, jadi dalam kasus spesifik Anda, selain mencoba memastikan tim menjadi lebih berpendidikan, saya tidak tahu.
sumber
Anda sudah berada di jalan dengan mengakui masalah dan secara aktif mencari solusi. Inilah yang akan saya lakukan:
sumber
Melihat contoh Anda: Segera setelah Foo :: Shutdown mulai dijalankan, Anda tidak mungkin memanggil OnHttpRequestComplete untuk menjalankannya lagi. Itu tidak ada hubungannya dengan implementasi apa pun, hanya saja tidak bisa.
Anda juga bisa berpendapat bahwa Foo :: Shutdown seharusnya tidak bisa dipanggil saat panggilan ke OnHttpRequestComplete berjalan (pasti benar) dan mungkin tidak jika panggilan ke OnHttpRequestComplete masih beredar.
Hal pertama yang harus dilakukan adalah tidak mengunci dll, tetapi logika dari apa yang diizinkan atau tidak. Model sederhana adalah bahwa kelas Anda mungkin memiliki nol atau lebih permintaan tidak lengkap, nol atau lebih penyelesaian yang belum dipanggil, nol atau lebih penyelesaian yang sedang berjalan, dan bahwa objek Anda ingin dimatikan atau tidak.
Foo :: Shutdown akan diharapkan untuk menyelesaikan menjalankan penyelesaian, untuk menjalankan permintaan yang tidak lengkap ke titik di mana mereka dapat dimatikan jika memungkinkan, untuk tidak mengizinkan penyelesaian lagi untuk dimulai, untuk tidak mengizinkan lebih banyak permintaan untuk dimulai.
Apa yang perlu Anda lakukan: Tambahkan spesifikasi ke fungsi Anda mengatakan dengan tepat apa yang akan mereka lakukan. (Misalnya, memulai permintaan http mungkin gagal setelah Shutdown dipanggil). Dan kemudian tulis fungsi Anda sehingga memenuhi spesifikasi.
Kunci paling baik digunakan hanya untuk jumlah waktu sekecil mungkin untuk mengendalikan modifikasi variabel bersama. Jadi, Anda mungkin memiliki variabel "PerformShutDown" yang dilindungi oleh Kunci.
sumber
Sejujurnya; Saya akan lari, cepat.
Masalah konkurensi yang jahat . Sesuatu dapat bekerja dengan sempurna selama berbulan-bulan dan kemudian (karena waktu spesifik beberapa hal) tiba-tiba meledak di wajah pelanggan, tanpa ada cara untuk mengetahui apa yang terjadi, tidak ada harapan untuk melihat laporan bug yang bagus (dapat diproduksi ulang) dan tidak mungkin bahkan untuk memastikan itu bukan kesalahan perangkat keras yang tidak ada hubungannya dengan perangkat lunak.
Menghindari masalah konkurensi harus dimulai selama fase desain, dimulai dengan bagaimana Anda akan melakukannya ("urutan kunci global", model aktor, ...). Itu bukan sesuatu yang Anda coba perbaiki dalam kepanikan gila dengan harapan bahwa semuanya tidak akan hancur sendiri setelah rilis yang akan datang.
Perhatikan bahwa saya tidak bercanda di sini. Kata-kata Anda sendiri (" Sebagian besar berasal dari pengembang lain yang telah meninggalkan tim. Pengembang saat ini dalam tim sangat cerdas, tetapi sebagian besar junior dalam hal pengalaman. ") Menunjukkan bahwa semua pengalaman yang dialami orang telah melakukan apa yang saya lakukan. sedang menyarankan.
sumber