Bagikan melalui


Mencegah Macet di Aplikasi Windows

Platform yang Terpengaruh

Klien - Windows 7
Server - Windows Server 2008 R2

Deskripsi

Hangs - Perspektif Pengguna

Pengguna menyukai aplikasi responsif. Ketika mereka mengklik menu, mereka ingin aplikasi bereaksi secara instan, bahkan jika saat ini sedang mencetak pekerjaan mereka. Ketika mereka menyimpan dokumen panjang dalam prosesor kata favorit mereka, mereka ingin terus mengetik saat disk masih berputar. Pengguna menjadi tidak sabar agak cepat ketika aplikasi tidak bereaksi secara tepat waktu terhadap input mereka.

Seorang programmer mungkin mengenali banyak alasan yang sah agar aplikasi tidak langsung merespons input pengguna. Aplikasi mungkin sibuk menghitung ulang beberapa data, atau hanya menunggu I/O disk selesai. Namun, dari penelitian pengguna, kita tahu bahwa pengguna merasa jengkel dan frustrasi hanya setelah beberapa detik tidak responsif. Setelah 5 detik, mereka akan mencoba mengakhiri aplikasi yang digantung. Di samping crash, aplikasi macet adalah sumber gangguan pengguna yang paling umum saat bekerja dengan aplikasi Win32.

Ada banyak akar penyebab yang berbeda untuk aplikasi macet, dan tidak semuanya memanifestasikan diri mereka dalam UI yang tidak responsif. Namun, UI yang tidak responsif adalah salah satu pengalaman macet yang paling umum, dan skenario ini saat ini menerima dukungan sistem operasi terbanyak untuk deteksi serta pemulihan. Windows secara otomatis mendeteksi, mengumpulkan informasi debug, dan secara opsional menghentikan atau memulai ulang aplikasi yang digantung. Jika tidak, pengguna mungkin harus menghidupkan ulang komputer untuk memulihkan aplikasi yang digantung.

Hangs - Perspektif Sistem Operasi

Ketika aplikasi (atau lebih akurat, utas) membuat jendela di desktop, aplikasi masuk ke dalam kontrak implisit dengan Desktop Window Manager (DWM) untuk memproses pesan jendela secara tepat waktu. DWM memposting pesan (input keyboard/mouse dan pesan dari jendela lain, serta pesan itu sendiri) ke dalam antrean pesan khusus utas. Utas mengambil dan mengirimkan pesan tersebut melalui antrean pesannya. Jika utas tidak melayani antrean dengan memanggil GetMessage(), pesan tidak diproses, dan jendela macet: tidak dapat menggambar ulang atau menerima input dari pengguna. Sistem operasi mendeteksi status ini dengan melampirkan timer ke pesan yang tertunda dalam antrean pesan. Jika pesan belum diambil dalam waktu 5 detik, DWM menyatakan jendela akan digantung. Anda dapat mengkueri status jendela khusus ini melalui API IsHungAppWindow().

Deteksi hanyalah langkah pertama. Pada titik ini, pengguna masih bahkan tidak dapat mengakhiri aplikasi - mengklik tombol X (Tutup) akan mengakibatkan pesan WM_CLOSE, yang akan terjebak dalam antrean pesan seperti pesan lainnya. Manajer Jendela Desktop membantu dengan menyembunyikan dengan lancar dan kemudian mengganti jendela yang digantung dengan salinan 'hantu' yang menampilkan bitmap area klien jendela asli sebelumnya (dan menambahkan "Tidak Merespons" ke bilah judul). Selama utas jendela asli tidak mengambil pesan, DWM mengelola kedua jendela secara bersamaan, tetapi memungkinkan pengguna untuk berinteraksi hanya dengan salinan hantu. Dengan menggunakan jendela hantu ini, pengguna hanya dapat memindahkan, meminimalkan, dan - yang paling penting - menutup aplikasi yang tidak responsif, tetapi tidak mengubah keadaan internalnya.

Seluruh pengalaman hantu terlihat seperti ini:

Cuplikan layar yang memperlihatkan dialog 'Notepad tidak merespons'.

Manajer Jendela Desktop melakukan satu hal terakhir; ini terintegrasi dengan Pelaporan Galat Windows, memungkinkan pengguna untuk tidak hanya menutup dan secara opsional memulai ulang aplikasi, tetapi juga mengirim data penelusuran kesalahan yang berharga kembali ke Microsoft. Anda bisa mendapatkan data macet ini untuk aplikasi Anda sendiri dengan mendaftar di situs web Winqual.

Windows 7 menambahkan satu fitur baru ke pengalaman ini. Sistem operasi menganalisis aplikasi yang macet dan, dalam keadaan tertentu, memberi pengguna opsi untuk membatalkan operasi pemblokiran dan membuat aplikasi responsif lagi. Implementasi saat ini mendukung pembatalan pemblokiran panggilan Soket; lebih banyak operasi akan dapat dibatalkan pengguna dalam rilis mendatang.

Untuk mengintegrasikan aplikasi Anda dengan pengalaman pemulihan macet dan memanfaatkan data yang tersedia secara maksimal, ikuti langkah-langkah berikut:

  • Pastikan aplikasi Anda mendaftar untuk memulai ulang dan pemulihan, membuat macet sedetail mungkin bagi pengguna. Aplikasi yang terdaftar dengan benar dapat dimulai ulang secara otomatis dengan sebagian besar data yang tidak disimpan secara utuh. Ini berfungsi untuk aplikasi macet dan crash.
  • Dapatkan informasi frekuensi serta debugging data untuk aplikasi Anda yang macet dan macet dari situs web Winqual. Anda dapat menggunakan informasi ini bahkan selama Beta Anda untuk meningkatkan kode Anda. Lihat "Memperkenalkan Pelaporan Galat Windows" untuk ringkasan singkat.
  • Anda dapat menonaktifkan fitur ghosting di aplikasi Anda melalui panggilan ke DisableProcessWindowsGhosting (). Namun, ini mencegah pengguna rata-rata menutup dan memulai ulang aplikasi yang digantung dan sering berakhir dengan boot ulang.

Hangs - Perspektif Pengembang

Sistem operasi mendefinisikan aplikasi macet sebagai utas UI yang belum memproses pesan setidaknya selama 5 detik. Bug yang jelas menyebabkan beberapa macet, misalnya, utas yang menunggu peristiwa yang tidak pernah diberi sinyal, dan dua utas masing-masing memegang kunci dan mencoba memperoleh yang lain. Anda dapat memperbaiki bug tersebut tanpa terlalu banyak usaha. Namun, banyak yang menggantung tidak begitu jelas. Ya, utas UI tidak mengambil pesan - tetapi sama-sama sibuk melakukan pekerjaan 'penting' lainnya dan akhirnya akan kembali memproses pesan.

Namun, pengguna dianggap ini sebagai bug. Desain harus sesuai dengan harapan pengguna. Jika desain aplikasi mengarah ke aplikasi yang tidak responsif, desain harus berubah. Akhirnya, dan ini penting, ketidakresponsifan tidak dapat diperbaiki seperti bug kode; ini membutuhkan pekerjaan di muka selama fase desain. Mencoba meretrofit basis kode aplikasi yang ada untuk membuat UI lebih responsif seringkali terlalu mahal. Panduan desain berikut mungkin membantu.

  • Jadikan responsI UI sebagai persyaratan tingkat atas; pengguna harus selalu merasa mengendalikan aplikasi Anda
  • Pastikan bahwa pengguna dapat membatalkan operasi yang membutuhkan waktu lebih dari satu detik untuk diselesaikan dan/atau bahwa operasi dapat diselesaikan di latar belakang; menyediakan UI kemajuan yang sesuai jika perlu

Cuplikan layar yang memperlihatkan dialog 'Menyalin item'.

  • Mengantre operasi jangka panjang atau memblokir sebagai tugas latar belakang (ini memerlukan mekanisme olahpesan yang dipikirkan dengan baik untuk menginformasikan utas UI ketika pekerjaan telah selesai)
  • Jaga agar kode untuk utas UI tetap sederhana; menghapus sebanyak mungkin pemblokiran panggilan API
  • Tampilkan jendela dan dialog hanya jika sudah siap dan beroperasi penuh. Jika dialog perlu menampilkan informasi yang terlalu intensif sumber daya untuk dihitung, tampilkan beberapa informasi umum terlebih dahulu dan perbarui dengan cepat saat lebih banyak data tersedia. Contoh yang baik adalah dialog properti folder dari Windows Explorer. Ini perlu menampilkan ukuran total folder, informasi yang tidak tersedia dari sistem file. Dialog segera muncul dan bidang "ukuran" diperbarui dari utas pekerja:

Cuplikan layar yang memperlihatkan halaman 'Umum' Properti Windows dengan teks 'Ukuran', 'Ukuran pada disk', dan 'Berisi' dilingkari.

Sayangnya, tidak ada cara sederhana untuk merancang dan menulis aplikasi responsif. Windows tidak menyediakan kerangka kerja asinkron sederhana yang akan memungkinkan penjadwalan pemblokiran yang mudah atau operasi yang berjalan lama. Bagian berikut memperkenalkan beberapa praktik terbaik dalam mencegah macet dan menyoroti beberapa jebakan umum.

Praktik Terbaik

Jaga Agar Utas UI Tetap Sederhana

Tanggung jawab utama utas UI adalah mengambil dan mengirimkan pesan. Jenis pekerjaan lainnya memperkenalkan risiko menggantung jendela yang dimiliki oleh utas ini.

Lakukan:

  • Memindahkan algoritma intensif sumber daya atau tidak terbatas yang mengakibatkan operasi jangka panjang ke utas pekerja
  • Identifikasi sebanyak mungkin panggilan fungsi pemblokiran dan coba pindahkan ke utas pekerja; setiap fungsi yang memanggil ke DLL lain harus mencurigakan
  • Lakukan upaya ekstra untuk menghapus semua I/O file dan panggilan API jaringan dari utas pekerja Anda. Fungsi-fungsi ini dapat memblokir selama banyak detik jika tidak menit. Jika Anda perlu melakukan segala jenis I/O di utas UI, pertimbangkan untuk menggunakan I/O asinkron
  • Ketahuilah bahwa utas UI Anda juga melayani semua server COM apartemen berulir tunggal (STA) yang dihosting oleh proses Anda; jika Anda melakukan panggilan pemblokiran, server COM ini akan tidak responsif sampai Anda melayani antrean pesan lagi

Jangan:

  • Tunggu pada objek kernel apa pun (seperti Peristiwa atau Mutex) selama lebih dari waktu yang sangat singkat; jika Anda harus menunggu sama sekali, pertimbangkan untuk menggunakan MsgWaitForMultipleObjects(), yang akan membuka blokir ketika pesan baru tiba
  • Bagikan antrean pesan jendela utas dengan utas lain dengan menggunakan fungsi AttachThreadInput(). Tidak hanya sangat sulit untuk menyinkronkan akses dengan benar ke antrean, itu juga dapat mencegah sistem operasi Windows mendeteksi jendela yang digantung dengan benar
  • Gunakan TerminateThread() pada salah satu utas pekerja Anda. Mengakhiri utas dengan cara ini tidak akan memungkinkannya untuk melepaskan kunci atau peristiwa sinyal dan dapat dengan mudah menghasilkan objek sinkronisasi tanpa sumber
  • Panggil kode 'tidak diketahui' dari utas UI Anda. Ini terutama berlaku jika aplikasi Anda memiliki model ekstensibilitas; tidak ada jaminan bahwa kode pihak ketiga mengikuti pedoman responsivitas Anda
  • Melakukan segala jenis pemblokiran panggilan siaran; SendMessage(HWND_BROADCAST) menempatkan Anda pada belas kasihan dari setiap aplikasi yang ditulis yang saat ini berjalan

Menerapkan Pola Asinkron

Menghapus operasi jangka panjang atau pemblokiran dari utas UI memerlukan penerapan kerangka kerja asinkron yang memungkinkan offload operasi tersebut ke utas pekerja.

Lakukan:

  • Gunakan API pesan jendela asinkron di utas UI Anda, terutama dengan mengganti SendMessage dengan salah satu rekan non-pemblokirannya: PostMessage, SendNotifyMessage, atau SendMessageCallback
  • Gunakan utas latar belakang untuk menjalankan tugas yang berjalan lama atau memblokir. Gunakan API kumpulan utas baru untuk mengimplementasikan utas pekerja Anda
  • Berikan dukungan pembatalan untuk tugas latar belakang yang berjalan lama. Untuk memblokir operasi I/O, gunakan pembatalan I/O, tetapi hanya sebagai upaya terakhir; tidak mudah untuk membatalkan operasi 'kanan'
  • Menerapkan desain asinkron untuk kode terkelola dengan menggunakan pola IAsyncResult atau dengan menggunakan Peristiwa

Gunakan Kunci Dengan Bijak

Aplikasi atau DLL Anda memerlukan kunci untuk menyinkronkan akses ke struktur data internalnya. Menggunakan beberapa kunci meningkatkan paralelisme dan membuat aplikasi Anda lebih responsif. Namun, menggunakan beberapa kunci juga meningkatkan kemungkinan untuk memperoleh kunci tersebut dalam urutan yang berbeda dan menyebabkan utas Anda mengalami kebuntuan. Jika dua utas masing-masing memegang kunci dan kemudian mencoba memperoleh kunci utas lainnya, operasi mereka akan membentuk tunggu melingkar yang memblokir kemajuan maju untuk utas ini. Anda dapat menghindari kebuntuan ini hanya dengan memastikan bahwa semua utas dalam aplikasi selalu memperoleh semua kunci dalam urutan yang sama. Namun, tidak selalu mudah untuk memperoleh kunci dalam urutan 'benar'. Komponen perangkat lunak dapat terdiri, tetapi akuisisi kunci tidak dapat. Jika kode Anda memanggil beberapa komponen lain, kunci komponen tersebut sekarang menjadi bagian dari urutan kunci implisit Anda - bahkan jika Anda tidak memiliki visibilitas ke dalam kunci tersebut.

Hal-hal menjadi lebih sulit karena operasi penguncian mencakup jauh lebih dari fungsi biasa untuk Bagian Kritis, Mutex, dan kunci tradisional lainnya. Setiap panggilan pemblokiran yang melewati batas alur memiliki properti sinkronisasi yang dapat mengakibatkan kebuntuan. Utas panggilan melakukan operasi dengan semantik 'acquire' dan tidak dapat membuka blokir sampai utas target 'rilis' panggilan tersebut. Beberapa fungsi User32 (misalnya SendMessage), serta banyak pemblokiran panggilan COM termasuk dalam kategori ini.

Lebih buruk lagi, sistem operasi memiliki kunci khusus proses internal sendiri yang terkadang ditahan saat kode Anda dijalankan. Kunci ini diperoleh ketika DLL dimuat ke dalam proses, dan oleh karena itu disebut 'kunci pemuat.' Fungsi DllMain selalu dijalankan di bawah kunci loader; jika Anda memperoleh kunci apa pun di DllMain (dan Anda tidak boleh), Anda perlu membuat bagian kunci pemuat dari urutan kunci Anda. Memanggil API Win32 tertentu mungkin juga memperoleh kunci loader atas nama Anda - fungsi seperti LoadLibraryEx, GetModuleHandle, dan terutama CoCreateInstance.

Untuk mengikat semua ini bersama-sama, lihat kode sampel di bawah ini. Fungsi ini memperoleh beberapa objek sinkronisasi dan secara implisit mendefinisikan urutan kunci, sesuatu yang belum tentu jelas pada inspeksi kursor. Pada entri fungsi, kode memperoleh Bagian Kritis dan tidak melepaskannya sampai fungsi keluar, sehingga menjadikannya simpul teratas dalam hierarki kunci kami. Kode kemudian memanggil fungsi Win32 LoadIcon(), yang di bawah sampul mungkin memanggil ke Pemuat Sistem Operasi untuk memuat biner ini. Operasi ini akan memperoleh kunci loader, yang sekarang juga menjadi bagian dari hierarki kunci ini (pastikan fungsi DllMain tidak memperoleh kunci g_cs). Selanjutnya kode memanggil SendMessage(), operasi pemblokiran lintas utas, yang tidak akan kembali kecuali utas UI merespons. Sekali lagi, pastikan bahwa utas UI tidak pernah memperoleh g_cs.

bool foo::bar (char* buffer)  
{  
      EnterCriticalSection(&g_cs);  
      // Get 'new data' icon  
      this.m_Icon = LoadIcon(hInst, MAKEINTRESOURCE(5));  
      // Let UI thread know to update icon SendMessage(hWnd,WM_COMMAND,IDM_ICON,NULL);  
      this.m_Params = GetParams(buffer);  
      LeaveCriticalSection(&g_cs);
      return true;  
}  

Melihat kode ini tampaknya jelas bahwa kita secara implisit membuat g_cs kunci tingkat atas dalam hierarki kunci kita, bahkan jika kita hanya ingin menyinkronkan akses ke variabel anggota kelas.

Lakukan:

  • Rancang hierarki kunci dan patuhi. Tambahkan semua kunci yang diperlukan. Ada lebih banyak primitif sinkronisasi daripada hanya Mutex dan CriticalSections; mereka semua perlu disertakan. Sertakan kunci loader dalam hierarki Anda jika Anda mengambil kunci apa pun di DllMain()
  • Setujui protokol penguncian dengan dependensi Anda. Kode apa pun yang dipanggil aplikasi Anda atau yang mungkin memanggil aplikasi Anda perlu berbagi hierarki kunci yang sama
  • Mengunci struktur data bukan fungsi. Pindahkan akuisisi kunci dari titik masuk fungsi dan jaga hanya akses data dengan kunci. Jika lebih sedikit kode beroperasi di bawah kunci, ada lebih sedikit kesempatan untuk kebuntuan
  • Analisis akuisisi dan rilis kunci dalam kode penanganan kesalahan Anda. Seringkali hierarki kunci jika dilupakan saat mencoba memulihkan dari kondisi kesalahan
  • Ganti kunci berlapis dengan penghitung referensi - kunci tersebut tidak dapat kebuntuan. Elemen yang dikunci secara individual dalam daftar dan tabel adalah kandidat yang baik
  • Berhati-hatilah saat menunggu handel utas dari DLL. Selalu asumsikan bahwa kode Anda dapat dipanggil di bawah kunci pemuat. Lebih baik menghitung referensi sumber daya Anda dan membiarkan utas pekerja melakukan pembersihan sendiri (dan kemudian menggunakan FreeLibraryAndExitThread untuk mengakhiri dengan bersih)
  • Gunakan WAIT Chain Traversal API jika Anda ingin mendiagnosis kebuntuan Anda sendiri

Jangan:

  • Lakukan apa pun selain pekerjaan inisialisasi yang sangat sederhana di fungsi DllMain() Anda. Lihat Fungsi Panggilan Balik DllMain untuk detail selengkapnya. Terutama jangan panggil LoadLibraryEx atau CoCreateInstance
  • Tulis primitif penguncian Anda sendiri. Kode sinkronisasi kustom dapat dengan mudah memperkenalkan bug halang ke dalam basis kode Anda. Gunakan pilihan objek sinkronisasi sistem operasi yang kaya sebagai gantinya
  • Lakukan pekerjaan apa pun di konstruktor dan destruktor untuk variabel global, mereka dijalankan di bawah kunci loader

Berhati-hatilah dengan Pengecualian

Pengecualian memungkinkan pemisahan aliran program normal dan penanganan kesalahan. Karena pemisahan ini, mungkin sulit untuk mengetahui keadaan program yang tepat sebelum pengecualian dan penangan pengecualian mungkin melewatkan langkah-langkah penting dalam memulihkan status yang valid. Ini terutama berlaku untuk akuisisi kunci yang perlu dirilis di handler untuk mencegah kebuntuan di masa depan.

Kode sampel di bawah ini menggambarkan masalah ini. Akses tidak terbatas ke variabel "buffer" terkadang akan mengakibatkan pelanggaran akses (AV). AV ini ditangkap oleh handler pengecualian asli, tetapi tidak memiliki cara mudah untuk menentukan apakah bagian kritis sudah diperoleh pada saat pengecualian (AV bahkan dapat terjadi di suatu tempat dalam kode EnterCriticalSection).

 BOOL bar (char* buffer)  
{  
   BOOL rc = FALSE;  
   __try {  
      EnterCriticalSection(&cs);  
      while (*buffer++ != '&') ;  
      rc = GetParams(buffer);  
      LeaveCriticalSection(&cs);  
   } __except (EXCEPTION_EXECUTE_HANDLER)  
   {  
      return FALSE;  
   } 
   return rc;  
}  

Lakukan:

  • Hapus __try/__except jika memungkinkan; jangan gunakan SetUnhandledExceptionFilter
  • Bungkus kunci Anda dalam templat kustom seperti auto_ptr jika Anda menggunakan pengecualian C++. Kunci harus dilepaskan di destruktor. Untuk pengecualian asli, lepaskan kunci dalam pernyataan __finally Anda
  • Berhati-hatilah dengan kode yang dijalankan dalam penangan pengecualian asli; pengecualian mungkin telah membocorkan banyak kunci, sehingga handler Anda tidak boleh memperoleh

Jangan:

  • Tangani pengecualian asli jika tidak diperlukan atau diperlukan oleh API Win32. Jika Anda menggunakan penangan pengecualian asli untuk pelaporan atau pemulihan data setelah kegagalan bencana, pertimbangkan untuk menggunakan mekanisme sistem operasi default Pelaporan Galat Windows sebagai gantinya
  • Gunakan pengecualian C++ dengan kode UI (user32) apa pun; pengecualian yang dilemparkan dalam panggilan balik akan melalui lapisan kode C yang disediakan oleh sistem operasi. Kode itu tidak tahu tentang semantik unroll C++