忙しくて待っています

コンピュータサイエンスソフトウェアエンジニアリングにおいて、ビジーウェイトビジーループ、またはスピニングとは、プロセスがキーボード入力やロックが使用可能かどうかなど、条件が真であるかどうかを繰り返し確認する手法である。また、スピニングは任意の時間遅延を生成するためにも使用でき、特定の時間待機する方法がないシステムでは必要な手法であった。プロセッサ速度はコンピュータごとに大きく異なり、特に一部のプロセッサは現在の作業負荷に基づいて速度を動的に調整するように設計されているためである。[ 1 ]そのため、時間遅延手法としてのスピニングは、プロセッサが「何もしない」ループを実行するのにかかる時間を決定するコードが含まれているか、ループコードが明示的にリアルタイムクロックをチェックしない限り、異なるシステムで一貫性のない、あるいは予測できない結果を生成する可能性がある。

多くの場合、スピニングはアンチパターンとみなされ、避けるべきです。[ 2 ]なぜなら、別のタスクを実行するために使用できるプロセッサ時間が、無駄なアクティビティに浪費されるからです。スピニングは、特定の状況では有効な戦略となる場合があり、特にSMPシステムで動作するように設計されたオペレーティングシステム内でのスピンロックの実装において有効です。

Cコードの例

以下のCコード例は、グローバル整数iを共有する2つのスレッドを示しています。最初のスレッドは、ビジーウェイトを使用して の値の変化を確認しますi

#include <pthread.h> #include <stdatomic.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h>/* i はグローバルなので、すべての関数から参照できます。アトミックメモリアクセスを可能にする特別な * 型 atomic_int を使用します*/ atomic_int i = 0 ;/* f1 はスピンロックを使用して i が 0 から変化するのを待ちます。 */ static void * f1 ( void * p ) { int local_i ; /* i の現在の値を local_i にアトミックにロードし、その値が 0 かどうかを確認します */ while (( local_i = atomic_load ( & i )) == 0 ) { /* 何もせず、何度もチェックを続けます */ }printf ( "iの値は%dに変更されました。\n " , local_i ); return NULL ; }static void * f2 ( void * p ) { int local_i = 99 ; sleep ( 10 ); /* 10秒間スリープします */ atomic_store ( & i , local_i ); printf ( "t2はiの値を%dに変更しました。\n " , local_i ); return NULL ; }int main () { int rc ; pthread_t t1 , t2 ;rc = pthread_create ( & t1 , NULL , f1 , NULL ); if ( rc != 0 ) { fprintf ( stderr , "pthread f1 が失敗しました\n " ); return EXIT_FAILURE ; }rc = pthread_create ( & t2 , NULL , f2 , NULL ); if ( rc != 0 ) { fprintf ( stderr , "pthread f2 が失敗しました\n " ); return EXIT_FAILURE ; }pthread_join ( t1 , NULL ); pthread_join ( t2 , NULL ); puts ( "すべてのpthreadが終了しました。" ); return 0 ; }

このようなユースケースでは、C11条件変数の使用を検討できます。

代替案

ほとんどのオペレーティングシステムとスレッドライブラリは、ロックの取得、タイマーの変更、 I/Oの可用性、シグナルなど、イベント発生時にプロセスをブロックする様々なシステムコールを提供しています。これらのシステムコールを使用すると、一般的に最も単純で効率的、公平、かつ競合のない結果が得られます。1回の呼び出しで、待機中のイベントを確認し、スケジューラに通知し、該当する場合はメモリバリアを挿入し、要求されたI/O操作を実行してから戻ります。呼び出し元がブロックされている間、他のプロセスはCPUを使用できます。スケジューラには、優先度の継承やその他のメカニズムを実装してリソース枯渇を回避するために必要な情報が提供されます。

ビジーウェイト自体は、ほとんどのオペレーティングシステムに備わっている遅延関数(例:sleep())を使用することで、無駄を大幅に減らすことができます。この関数は、指定された時間だけスレッドをスリープ状態にし、その間スレッドはCPU時間を無駄にしません。ループが単純な何かをチェックしている場合、ほとんどの時間をスリープ状態にして、CPU時間を無駄にすることはほとんどありません。

終了しないプログラム (オペレーティング システムなど) では、次のNASM構文に示すように、無条件ジャンプを使用して無限ビジー待機を実装できます。

ジャンプ$

CPUは無条件に自身の位置ジャンプし続けます。このようなビジーウェイトは次のように置き換えることができます。

スリープ: hlt jmpスリープ

詳細については、「HLT (x86 命令)」を参照してください。

適切な使用

低レベルプログラミングでは、ビジーウェイトが実際には望ましい場合もあります。すべてのハードウェアデバイス、特にアクセス頻度の低いデバイスに割り込み駆動型処理を実装することは、必ずしも望ましいことではなく、現実的でもありません。場合によっては、何らかの制御データをハードウェアに書き込み、その書き込み操作の結果であるデバイスステータスを取得する必要があります。このステータスは、書き込み後、一定数のマシンサイクルが経過するまで有効にならない場合があります。プログラマはオペレーティングシステムの遅延関数を呼び出すこともできますが、デバイスがステータスを返すのを数クロックサイクル待つよりも多くの時間を消費する可能性があります。

参照

参考文献