正しく眠る方法

少し前まで、現代のソフトウェアのパフォーマンスのひどい状態( 英語の 原文、Habréの翻訳 )についての良い記事が私たちを通り過ぎました 。 この記事では、コードのアンチパターンの1つを思い出しました。これは非常に一般的であり、通常は何らかの形で機能しますが、あちこちでパフォーマンスがわずかに低下します。 さて、あなたは知っています、些細なことです、どの手も手を伸ばすことはありません。 唯一の問題は、コード内のさまざまな場所に散らばっているこれらの「些細なこと」の多くが、「最新のIntel Core i7があり、スクロールする動きがある」などの問題につながることです。



スリープ機能の不適切な使用について説明しています(ケースはプログラミング言語とプラットフォームによって異なる場合があります)。 では、スリープとは何ですか? ドキュメントはこの質問に非常に簡単に答えています。これは、指定されたミリ秒数の間、現在のスレッドの実行を一時停止することです。 この関数のプロトタイプの審美的な美しさに注意する必要があります。



void Sleep(DWORD dwMilliseconds);
      
      





1つのパラメーター(非常に明確)、エラーコードや例外はありません-常に機能します。 そのような素晴らしくて理解可能な機能はほとんどありません!



どのように機能するかを読むと、この機能をさらに尊重することになります。
この関数はOSスレッドスケジューラに移動し、次のように伝えます。「私のスレッドと、現在および今後数ミリ秒の間、割り当てられたCPU時間を拒否したいと思います。 貧しい人々に与えてください!」 このような寛大さに少し驚いたスケジューラーは、プロセッサーに代わって感謝の機能を取り出し、残りの時間を次の人に与えます(そして常に存在します)。 美人!



何が間違っていたのでしょうか? プログラマーがこの素晴らしい機能を使用しているという事実は、それが意図されているものではありません。



また、実際の一時停止プロセスによって定義される外部のソフトウェアシミュレーションを対象としています。



正しい例番号1



「クロック」アプリケーションを作成しています。このアプリケーションでは、画面上の数字(または矢印の位置)を1秒ごとに変更する必要があります。 ここでのスリープ機能は完全に適合しています。明確に定義された期間(正確には1秒)の間、実際には何の関係もありません。 なぜ寝ないの?



正しい例2



私たちはパン製造用の密造酒コントローラー書いています。 操作アルゴリズムはプログラムの1つによって設定され、次のようになります。



  1. モード1に進みます。
  2. 20分間作業する
  3. モード2に進みます。
  4. 10分間働きます
  5. オフにします。


ここのすべても明確です。私たちは時間とともに作業します。それは技術的プロセスによって設定されます。 スリープの使用は許容されます。



次に、スリープの誤用の例を見てみましょう。



誤ったC ++コードの例が必要な場合はメモ帳++テキストエディターのコードリポジトリに移動します 。 そのコードはとてもひどいので、アンチパターンは間違いなくそこにあります。 今回もNotepad ++で私を失望させませんでした! Sleepの使用方法を見てみましょう。



悪い例番号1



起動時に、Notepad ++はそのプロセスの別のインスタンスが既に実行されているかどうかを確認し、実行されている場合はウィンドウを探してメッセージを送信し、閉じます。 別のプロセスを検出するために、標準的な方法-ミューテックスという名前のグローバルが使用されます。 ただし、ウィンドウを検索するために次のコードが作成されました。



 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) { ... } ... }
      
      







このコードを書いたプログラマーは、既に起動されているメモ帳++のウィンドウを見つけようとし、2つのプロセスが文字通り同時に開始される状況を想定していました。そのため、最初のプロセスはすでにグローバルミューテックスを作成していましたが、まだエディターウィンドウを作成していませんでした。 この場合、2番目のプロセスは「100 msで5回」ウィンドウの作成を待機します。 その結果、実際にウィンドウが作成されてからスリープが終了するまでの間に、まったく待機しないか、最大100 msが失われます。



これは、Sleepを使用する最初の(そして主要な)パターンの1つです。 イベントの発生を待っていませんが、「数ミリ秒の間、突然ラッキーになります」。 私たちは非常に待っているので、一方で私たちはユーザーを本当に困らせず、他方では、必要なイベントを待つ機会があります。 はい、ユーザーはアプリケーションの起動時に100ミリ秒の一時停止に気付かない場合があります。 しかし、「ブルドーザーから少し待つ」というこのような慣行がプロジェクトで受け入れられ、受け入れられる場合、最も些細な理由ですべての段階で待つという事実で終わる可能性があります。 ここで、100ミリ秒、さらに50ミリ秒、そしてここで200ミリ秒があります-そして、ここで私たちのプログラムはすでに「なんとか数秒間減速しています」。



さらに、長時間動作するコードがすぐに動作するのを見るのは、単に見た目が悪いだけです。 この特定のケースでは、HSHELL_WINDOWCREATEDイベントにサブスクライブしてSetWindowsHookEx関数を使用し、ウィンドウ作成の通知を即座に受信できます。 はい、コードはもう少し複雑になりますが、文字通り3〜4行です。 そして、最大100ミリ秒で勝ちます! そして最も重要なことは、期待が無条件ではない場合、無条件の期待の機能を使用しないことです。



悪い例番号2



 HANDLE hThread = ::CreateThread(NULL, 0, threadTextTroller, &trollerParams, 0, NULL); int sleepTime = 1000 / x * y; ::Sleep(sleepTime);
      
      





Notepad ++でこのコードが何をどのくらい待っているのか、正確にはわかりませんでしたが、「ストリームを開始して待つ」という一般的なアンチパターンをよく見ました。 人々はさまざまなことを期待しています。別のストリームの開始、ストリームからのデータの受信、作業の終了です。 ここですぐに悪いことが2つあります。



  1. マルチスレッドを行うには、マルチスレッドプログラミングが必要です。 つまり 2番目のスレッドの起動は、最初のスレッドで何かを続けることを前提としています。この時点で、2番目のスレッドは他の作業を行い、最初のスレッドはその作業を終えた後(そして、おそらくもう少し待った後)、結果を取得して何らかの方法で使用します。 2番目のスレッドを開始した直後に「スリープ」を開始する場合-なぜそれが必要なのですか?
  2. 正しいことを期待してください。 適切な期待のために、実証済みのプラクティスがあります:イベントの使用、待機関数、コールバックの呼び出し。 コードが2番目のスレッドで動作を開始するのを待っている場合は、このためのイベントを設定し、2番目のスレッドで通知します。 2番目のスレッドが動作を終了するのを待っている場合-C ++には素晴らしいスレッドクラスとその結合メソッドがあります(まあ、または、WindowsのWaitForSingleObjectやHANDLEのようなプラットフォーム固有のメソッドです)。 別のスレッドで作業が「数ミリ秒」完了するのを待つのは、単に愚かです。なぜなら、リアルタイムOSがない場合、2番目のスレッドが開始されるか、作業のある段階に達する時間を保証する人はいないからです。


悪い例3



ここでは、いくつかのイベントを待ってスリープしているバックグラウンドスレッドが表示されます。



 class CReadChangesServer { ... void Run() { while (m_nOutstandingRequests || !m_bTerminate) { ::SleepEx(INFINITE, true); } } ... void RequestTermination() { m_bTerminate = true; ... } ... bool m_bTerminate; };
      
      





ここで使用されているのはSleepではなく、よりインテリジェントで一部のイベント(非同期操作の完了など)の待機を中断できるSleepExであることを認めなければなりません。 しかし、これはまったく役に立ちません! 実際、while(!M_bTerminate)ループには無限に機能するすべての権利があり、別のスレッドから呼び出されたRequestTermination()メソッドを無視して、m_bTerminate変数をtrueにリセットします。 これの原因と結果については、 以前の記事で書きました。 これを回避するには、スレッド間で正常に動作することが保証されているもの(アトミック、イベントなど)を使用する必要があります。



はい、正式には、SleepExはスレッドを同期するために通常のブール変数を使用する問題のせいではありません;これは別のクラスの別のエラーです。 しかし、なぜこのコードで可能になったのでしょうか? というのも、最初はプログラマーが「ここで寝る必要がある」と考えてから、これをやめるのにどれだけの時間と条件が必要かを考えたからです。 そして、適切なシナリオでは、彼は最初に考える必要さえありません。 「イベントを待たなければならない」という考えが頭に浮かぶはずでした。その瞬間から、フロー間でデータを同期するための適切なメカニズムを選択することになり、ブール変数とSleepExの使用の両方が除外されます。



悪い例番号4



この例では、「自動保存」として機能するbackupDocument関数を調べます。これは、予期しないエディタークラッシュの場合に役立ちます。 デフォルトでは、彼女は7秒間スリープし、その後、変更を保存するコマンドを発行します(変更があった場合)。



 DWORD WINAPI Notepad_plus::backupDocument(void * /*param*/) { ... while (isSnapshotMode) { ... ::Sleep(DWORD(timer)); ... ::PostMessage(Notepad_plus_Window::gNppHWND, NPPM_INTERNAL_SAVEBACKUP, 0, 0); } return TRUE; }
      
      





間隔は変更できますが、これは問題ではありません。 どの間隔も、同時に長すぎたり短すぎたりします。 1分間に1文字を入力する場合、7秒間だけスリープするのは意味がありません。 どこかから10メガバイトのテキストをコピーして貼り付けた場合、その後7秒待つ必要はありません。すぐにバックアップを開始するのに十分な大きさです(突然どこかでそれを切り取り、そこから消えて、1秒後にエディターがクラッシュします)。



つまり 単純な期待で、欠落しているよりインテリジェントなアルゴリズムをここで置き換えます。



悪い例番号5



メモ帳++は「テキストを入力」できます-つまり 文字挿入の間に一時停止することにより、人間のテキスト入力をエミュレートします。 「イースターエッグ」として書かれているようですが、この機能の何らかの実用的なアプリケーションを考え出すことができます( Upworkをだます )。



 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);
      
      





ここでの問題は、各キーを押すたびにコードが400から800 msの間一時停止する「平均的な人」のようなアイデアを持っていることです。 わかりました、多分それは「平均」および正常です。 しかし、あなたが知っているのは、私が使用するプログラムが単に彼女の美しく適切なように見えるからといって、私の仕事を一時停止する場合です-これは彼女の意見を共有するという意味ではありません。 一時停止データの期間を調整できるようにしたいと思います。 また、Notepad ++の場合、これがそれほど重要ではない場合、他のプログラムでは、「データを更新する:頻繁に、通常、まれに」などの設定に遭遇することがあります。めったにありません。 はい、「正常」は正常ではありませんでした。 このような機能により、ユーザーは目的のアクションが実行されるまで待機するミリ秒数を正確に示すことができます。 「0」を入力する必須オプションを使用します。 さらに、この場合の0はSleep関数への引数としても渡されるべきではなく、単にその呼び出しを除外するためです(Sleep(0)は実際には即座に戻りませんが、スケジューラによって与えられたタイムスロットの残りの部分を別のスレッドに与えます)。



結論



スリープを使用すると、特定の期間に無条件に指定された期待値であり、「技術プロセスに従って」、「この式に従って時間を計算します」、「そんなに待たない」という論理的な説明がある場合、期待値を満たすことができます。顧客は言った。 一部のイベントの待機またはスレッドの同期は、スリープ機能を使用して実装しないでください。



All Articles