Belum lama ini, sebuah artikel bagus mengalir melewati kami tentang kondisi kinerja perangkat lunak modern yang mengerikan (
asli dalam bahasa Inggris ,
terjemahan dalam bahasa Habrรฉ ). Artikel ini mengingatkan saya pada salah satu antipattern dari kode, yang sangat umum dan umumnya berfungsi, tetapi menyebabkan kerugian kinerja kecil di sana-sini. Nah, Anda tahu, hal sepele, yang tangan tidak akan menggapai dengan cara apa pun. Satu-satunya masalah adalah bahwa selusin "hal-hal sepele" ini yang tersebar di berbagai tempat dalam kode mulai menimbulkan masalah seperti "Saya memiliki Intel Core i7 terbaru, dan menggulir kedutan."

Saya berbicara tentang penggunaan fungsi
Tidur yang tidak benar (kasing dapat bervariasi tergantung pada bahasa pemrograman dan platform). Jadi, apa itu Tidur? Dokumentasi menjawab pertanyaan ini dengan sangat sederhana: ini adalah jeda dalam pelaksanaan utas saat ini untuk jumlah milidetik yang ditentukan. Perlu dicatat keindahan estetika dari prototipe fungsi ini:
void Sleep(DWORD dwMilliseconds);
Hanya satu parameter (sangat jelas), tidak ada kode kesalahan atau pengecualian - selalu berfungsi. Ada beberapa fungsi yang bagus dan mudah dimengerti!
Anda mendapatkan lebih banyak rasa hormat untuk fungsi ini ketika Anda membaca cara kerjanya.Fungsi ini pergi ke scheduler thread OS dan memberitahu itu, โThread saya dan saya ingin menolak waktu CPU yang dialokasikan untuk kami, sekarang dan untuk milidetik di masa depan. Berikan kepada orang miskin! " Penjadwal, yang sedikit terkejut dengan kemurahan hati seperti itu, mengeluarkan fungsi rasa terima kasih atas nama prosesor, memberikan sisa waktu kepada orang berikutnya (dan selalu ada yang) dan tidak termasuk utas yang menyebabkan Sleep berpura-pura mengirimkan konteks eksekusi untuk jumlah milidetik yang ditentukan. Cantik!
Apa yang salah? Fakta bahwa pemrogram menggunakan fitur yang luar biasa ini bukan untuk apa yang dimaksudkan.
Dan itu dimaksudkan untuk simulasi perangkat lunak dari beberapa eksternal, yang ditentukan oleh sesuatu yang nyata, proses jeda.
Contoh yang benar nomor 1
Kami sedang menulis aplikasi "jam", di mana satu detik sekali Anda perlu mengubah nomor di layar (atau posisi panah). Fungsi Tidur di sini sangat cocok: kami benar-benar tidak ada hubungannya untuk jangka waktu yang jelas (tepat satu detik). Kenapa tidak tidur?
Contoh yang benar nomor 2
Kami sedang menulis pengontrol
nonsen untuk mesin roti. Algoritma operasi diatur oleh salah satu program dan terlihat seperti ini:
- Pergi ke mode 1.
- Kerjakan selama 20 menit
- Pergi ke mode 2.
- Kerjakan selama 10 menit
- Matikan.
Semuanya di sini juga jelas: kita bekerja dengan waktu, itu diatur oleh proses teknologi. Menggunakan Sleep dapat diterima.
Sekarang mari kita lihat contoh penyalahgunaan Tidur.
Ketika saya membutuhkan beberapa contoh kode C ++ yang salah, saya pergi ke
repositori kode editor teks Notepad ++. Kodenya sangat mengerikan sehingga antipattern pasti ada di sana, saya bahkan pernah menulis
artikel tentang ini. Notepad ++ juga tidak mengecewakan saya! Mari kita lihat bagaimana menggunakan Sleep.
Contoh buruk nomor 1
Saat startup, Notepad ++ memeriksa untuk melihat apakah instance lain dari prosesnya sudah berjalan dan, jika demikian, mencari jendelanya dan mengirimkannya pesan, dan ditutup. Untuk mendeteksi proses lain, metode standar digunakan - global bernama mutex. Tetapi kode berikut ditulis untuk mencari windows:
if ((!isMultiInst) && (!TheFirstOne)) { HWND hNotepad_plus = ::FindWindow(Notepad_plus_Window::getClassName(), NULL); for (int i = 0 ;!hNotepad_plus && i < 5 ; ++i) { Sleep(100); hNotepad_plus = ::FindWindow(Notepad_plus_Window::getClassName(), NULL); } if (hNotepad_plus) { ... } ... }
Programmer yang menulis kode ini mencoba menemukan jendela untuk Notepad ++ yang sudah diluncurkan dan bahkan membayangkan situasi di mana dua proses dimulai secara harfiah pada saat yang sama, jadi yang pertama dari mereka sudah membuat global mutex, tetapi belum membuat jendela editor. Dalam hal ini, proses kedua akan menunggu pembuatan jendela "5 kali dalam 100 ms". Akibatnya, kami tidak akan menunggu sama sekali, atau kehilangan hingga 100 ms antara saat pembuatan jendela sebenarnya dan keluar dari Sleep.
Ini adalah antipattern pertama (dan salah satu yang utama) menggunakan Sleep. Kami tidak menunggu terjadinya acara, tetapi "selama beberapa milidetik, tiba-tiba itu akan beruntung." Kami sangat menunggu sehingga di satu sisi kami tidak benar-benar mengganggu pengguna, dan di sisi lain, kami memiliki kesempatan untuk menunggu acara yang kami butuhkan. Ya, pengguna mungkin tidak melihat jeda 100 ms saat memulai aplikasi. Tetapi jika praktik "menunggu sedikit dari buldoser" diterima dan diizinkan dalam proyek, ini mungkin berakhir dengan fakta bahwa kami akan menunggu di setiap langkah untuk alasan yang paling sepele. Di sini 100 ms, ada 50 ms lagi, dan di sini 200 ms - dan di sini program kami sudah "entah bagaimana melambat selama beberapa detik."
Selain itu, secara estetika tidak menyenangkan melihat kode yang berjalan untuk waktu yang lama sementara itu bisa bekerja dengan cepat. Dalam kasus khusus ini, seseorang dapat menggunakan fungsi
SetWindowsHookEx dengan berlangganan ke acara HSHELL_WINDOWCREATED - dan menerima pemberitahuan pembuatan jendela secara instan. Ya, kode menjadi sedikit lebih rumit, tetapi secara harfiah 3-4 baris. Dan kami menang hingga 100 ms! Dan yang paling penting - kita tidak lagi menggunakan fungsi harapan tanpa syarat di mana harapan itu tidak tanpa syarat.
Contoh buruk nomor 2
HANDLE hThread = ::CreateThread(NULL, 0, threadTextTroller, &trollerParams, 0, NULL); int sleepTime = 1000 / x * y; ::Sleep(sleepTime);
Saya tidak benar-benar mengerti apa sebenarnya dan untuk berapa lama kode ini menunggu di Notepad ++, tetapi saya sering melihat antipattern umum untuk "memulai aliran dan menunggu". Orang-orang mengharapkan hal-hal berbeda: mulai dari aliran lain, penerimaan beberapa data darinya, akhir pekerjaannya. Dua hal buruk di sini segera:
- Pemrograman multithreaded diperlukan untuk melakukan sesuatu multithreaded. Yaitu peluncuran utas kedua mengasumsikan bahwa kita akan terus melakukan sesuatu pada yang pertama, pada saat ini utas kedua akan melakukan pekerjaan lain, dan yang pertama, setelah menyelesaikan pekerjaannya (dan, mungkin, setelah menunggu lebih lama), akan mendapatkan hasilnya dan menggunakannya entah bagaimana. Jika kita mulai "tidur" segera setelah memulai utas kedua - mengapa itu diperlukan sama sekali?
- Perlu untuk berharap dengan benar. Untuk ekspektasi yang tepat, ada praktik yang terbukti: penggunaan acara, fungsi tunggu, panggilan balik. Jika kami menunggu kode untuk mulai bekerja di utas kedua, atur acara untuk ini dan beri sinyal pada utas kedua. Jika kita menunggu utas kedua selesai bekerja - dalam C ++ ada kelas utas luar biasa dan metode penggabungannya (well, atau, sekali lagi, metode khusus platform seperti WaitForSingleObject dan HANDLE pada Windows). Menunggu pekerjaan diselesaikan di utas lain "selama beberapa milidetik" benar-benar bodoh, karena jika kita tidak memiliki OS waktu nyata, tidak ada yang akan memberi Anda jaminan berapa lama utas kedua akan mulai atau mencapai tahap tertentu dari pekerjaannya.
Contoh buruk nomor 3
Di sini kita melihat utas latar belakang yang sedang tidur menunggu beberapa acara.
class CReadChangesServer { ... void Run() { while (m_nOutstandingRequests || !m_bTerminate) { ::SleepEx(INFINITE, true); } } ... void RequestTermination() { m_bTerminate = true; ... } ... bool m_bTerminate; };
Saya harus mengakui bahwa bukan Sleep yang digunakan di sini, melainkan
SleepEx , yang lebih cerdas dan dapat mengganggu menunggu beberapa peristiwa (seperti penyelesaian operasi asinkron). Tetapi ini sama sekali tidak membantu! Faktanya adalah bahwa while (! M_bTerminate) memiliki hak untuk bekerja tanpa henti, mengabaikan metode RequestTermination () dipanggil dari utas lainnya, mengatur ulang variabel m_bTerminate menjadi true. Saya menulis tentang sebab dan akibat dari hal ini di
artikel sebelumnya . Untuk menghindari ini, Anda harus menggunakan sesuatu yang dijamin berfungsi dengan baik di antara utas: atomik, peristiwa, atau yang serupa.
Ya, secara resmi SleepEx tidak bisa disalahkan atas masalah menggunakan variabel Boolean yang biasa untuk menyinkronkan utas, ini adalah kesalahan terpisah dari kelas lain. Tetapi mengapa itu menjadi mungkin dalam kode ini? Karena pada awalnya programmer berpikir "kamu perlu tidur di sini", dan kemudian berpikir tentang berapa lama dan dalam kondisi apa untuk berhenti melakukan ini. Dan dalam skenario yang tepat, dia seharusnya tidak memiliki pemikiran pertama. Pikiran "harus menunggu acara" seharusnya muncul di kepala saya - dan mulai sekarang, pikiran itu akan bekerja untuk memilih mekanisme yang tepat untuk menyinkronkan data di antara utas, yang akan mengecualikan variabel Boolean dan penggunaan SleepEx.
Contoh buruk nomor 4
Dalam contoh ini, kita akan melihat fungsi backupDocument, yang bertindak sebagai "penyimpanan otomatis", berguna jika terjadi crash editor yang tidak terduga. Secara default, dia tidur selama 7 detik, lalu memberikan perintah untuk menyimpan perubahan (jika ada).
DWORD WINAPI Notepad_plus::backupDocument(void * ) { ... while (isSnapshotMode) { ... ::Sleep(DWORD(timer)); ... ::PostMessage(Notepad_plus_Window::gNppHWND, NPPM_INTERNAL_SAVEBACKUP, 0, 0); } return TRUE; }
Interval dapat diubah, tetapi ini bukan masalahnya. Interval apa pun akan terlalu panjang dan terlalu pendek pada saat bersamaan. Jika kita mengetikkan satu huruf per menit, tidak masuk akal untuk tidur hanya 7 detik. Jika kami menyalin-tempel 10 megabita teks dari suatu tempat, kami tidak perlu menunggu 7 detik setelah itu, itu adalah volume yang cukup besar untuk segera memulai pencadangan (tiba-tiba kami hentikan itu dari suatu tempat dan hilang di sana, dan editor mogok setelah satu detik).
Yaitu dengan harapan sederhana, kami mengganti algoritma yang lebih cerdas yang hilang di sini.
Contoh buruk nomor 5
Notepad ++ dapat "mengetik teks" - mis. meniru input teks manusia dengan menjeda di antara penyisipan huruf. Tampaknya ditulis sebagai "telur Paskah", tetapi Anda dapat menemukan beberapa jenis aplikasi yang berfungsi dari fitur ini (
menipu Upwork, ya ).
int pauseTimeArray[nbPauseTime] = {200,400,600}; const int maxRange = 200; ... int ranNum = getRandomNumber(maxRange); ::Sleep(ranNum + pauseTimeArray[ranNum%nbPauseTime]); ::SendMessage(pCurrentView->getHSelf(), SCI_DELETEBACK, 0, 0);
Masalahnya di sini adalah bahwa kode memiliki ide semacam "orang biasa" berhenti selama 400-800 ms antara setiap tombol yang ditekan. Ok, mungkin itu "rata-rata" dan normal. Tapi tahukah Anda, jika program yang saya gunakan membuat beberapa jeda dalam pekerjaan saya hanya karena mereka tampak cantik dan cocok untuknya - ini tidak berarti sama sekali bahwa saya membagikan pendapatnya. Saya ingin dapat menyesuaikan durasi data jeda. Dan, jika dalam kasus Notepad ++ ini tidak terlalu kritis, maka di program lain saya kadang-kadang bertemu pengaturan seperti "memperbarui data: sering, biasanya, jarang", di mana "sering" tidak cukup untuk saya sering, dan "jarang" tidak cukup jarang. Ya, dan "normal" tidak normal. Fungsionalitas seperti itu harus memungkinkan pengguna untuk secara akurat menunjukkan jumlah milidetik yang ia ingin tunggu sampai tindakan yang diinginkan selesai. Dengan opsi wajib untuk memasukkan "0". Selain itu, 0 dalam hal ini bahkan tidak boleh diteruskan sebagai argumen ke fungsi Sleep, tetapi cukup mengecualikan panggilannya (Sleep (0) tidak benar-benar kembali secara instan, tetapi memberikan sisa potongan slot waktu yang diberikan oleh penjadwal ke utas lain).
Kesimpulan
Dengan bantuan Sleep, seseorang dapat dan harus memenuhi harapan ketika itu adalah harapan yang diberikan tanpa syarat untuk periode waktu tertentu dan ada beberapa penjelasan logis mengapa seperti ini: "sesuai dengan proses teknologi", "waktu dihitung berdasarkan rumus ini", "tunggu begitu banyak kata pelanggan. " Menunggu beberapa acara atau utas sinkronisasi tidak boleh dilaksanakan menggunakan fungsi Sleep.