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?