マイクロソフト固有のC++コードを作成するときに、スピンロックの場合 Sleep(1)
の方がSleep(0)
よりもはるかに優れていると言われました。これは、Sleep(0)
は、より多くのCPU時間を使用します。さらに、実行を待機している別の等しい優先順位スレッドがある場合にのみ、CPU時間を生成します。
ただし、C++ 11スレッドライブラリでは、std::this_thread::yield()
とstd::this_thread::sleep_for( std::chrono::milliseconds(1) )
の効果についてのドキュメントはほとんどありません(少なくとも私は見つけることができました)。 2番目は確かにより詳細ですが、どちらもスピンロックに対して同等に効率的ですか、またはSleep(0)
とSleep(1)
に影響を与えた同じ問題の可能性がありますか?
std::this_thread::yield()
またはstd::this_thread::sleep_for( std::chrono::milliseconds(1) )
のどちらでも受け入れられるループの例:
void SpinLock( const bool& bSomeCondition )
{
// Wait for some condition to be satisfied
while( !bSomeCondition )
{
/*Either std::this_thread::yield() or
std::this_thread::sleep_for( std::chrono::milliseconds(1) )
is acceptable here.*/
}
// Do something!
}
具体的な実装は、基礎となるオペレーティングシステムのスケジューリング機能に大きく影響されるため、ここでは標準がややあいまいです。
そうは言っても、最新のOSではいくつかのことを安全に想定できます。
yield
は現在のタイムスライスをあきらめ、スレッドをスケジューリングキューに再挿入します。スレッドが再度実行されるまでに期限が切れる時間は、通常、完全にスケジューラーに依存します。基準は歩留まりを再スケジュールの機会として説明していることに注意してください。したがって、必要に応じて、実装はすぐに利回りから完全に戻ることができます。収量がスレッドを非アクティブとしてマークすることは決してないため、収量で回転するスレッドは常に1つのコアに100%の負荷を生成します。他のスレッドの準備ができていない場合、再度スケジュールされる前に、現在のタイムスライスの残りを最大で失う可能性があります。sleep_*
_は、少なくとも要求された時間スレッドをブロックします。実装はsleep_for(0)
をyield
に変えることができます。一方、sleep_for(1)
はスレッドを一時停止します。スレッドは、スケジューリングキューに戻る代わりに、最初にスリープ状態のスレッドの別のキューに移動します。要求された時間が経過した後にのみ、スケジューラはスレッドをスケジューリングキューに再挿入することを検討します。小さな睡眠による負荷は依然として非常に高くなります。要求されたスリープ時間がシステムのタイムスライスよりも小さい場合、スレッドは1つのタイムスライスだけをスキップする(つまり、アクティブなタイムスライスを解放するために1つの利回りを獲得し、その後に1つをスキップする)と予想できますが、それでもCPU負荷につながります。 1つのコアで100%に近い、または100%に等しい。どちらがスピンロックに適しているかについて少し説明します。スピンロックは、ロックの競合がほとんどないかまったくない場合に最適なツールです。ほとんどの場合、ロックが使用可能であると予想される場合、スピンロックは安価で価値のあるソリューションです。ただし、競合が発生するとすぐに、スピンロックwillがかかります。ここで、yieldとsleepのどちらがより良いソリューションであるかを心配している場合、スピンロックはジョブの間違ったツールです。代わりにミューテックスを使用する必要があります。
スピンロックの場合、実際にロックを待たなければならないケースは例外的と見なされるべきです。したがって、ここで譲ることは完全に問題ありません。意図を明確に表し、CPU時間の浪費がそもそも問題になることはありません。
私はWindows 7、2.8GHz Intel i7、デフォルトのリリースモード最適化でVisual Studio 2013を使用してテストを行いました。
sleep_for(nonzero)は、最小で約1ミリ秒間スリープ状態になり、次のようなループでCPUリソースを消費しません。
for (int k = 0; k < 1000; ++k)
std::this_thread::sleep_for(std::chrono::nanoseconds(1));
1,000スリープのこのループは、1ナノ秒、1マイクロ秒、または1ミリ秒を使用する場合、約1秒かかります。一方、yield()はそれぞれ約0.25マイクロ秒かかりますが、スレッドのCPUを100%にスピンします。
for (int k = 0; k < 4,000,000; ++k) (commas added for clarity)
std::this_thread::yield();
std :: this_thread :: sleep_for((std :: chrono :: nanoseconds(0))は、yield()とほぼ同じように見えます(テストはここには表示されていません)。
比較すると、スピンロックのatomic_flagのロックには、約5ナノ秒かかります。このループは1秒です。
std::atomic_flag f = ATOMIC_FLAG_INIT;
for (int k = 0; k < 200,000,000; ++k)
f.test_and_set();
また、ミューテックスは約50ナノ秒、このループでは1秒かかります。
for (int k = 0; k < 20,000,000; ++k)
std::lock_guard<std::mutex> lock(g_mutex);
これに基づいて、おそらくスピンロックに利回りを入れることをためらわないでしょうが、ほぼ確実にsleep_forを使用しません。ロックが頻繁に回転していると思い、CPUの消費が心配な場合は、アプリケーションで実用的であればstd :: mutexに切り替えます。うまくいけば、Windowsのstd :: mutexのパフォーマンスが本当に悪かった時代は私たちの背後にあります。
yieldを使用しているときにCPUロードに興味がある場合-1つのケースを除いて、それは非常に悪いです(アプリケーションのみが実行されており、基本的にすべてのリソースを消費することを知っています)
ここに詳細な説明があります:
sleep()
またはsleep_for()
の実行も誤りです。これはスレッドの実行をブロックしますが、CPUの待機時間のようなものになります。誤解しないでください。これは、IS実行中のCPUですが、優先度は最低です。簡単な使用例では何とか機能しています(sleep()で完全にロードされたCPUは、完全にロードされた実行中のプロセッサの半分です) 、アプリケーションの責任を保証したい場合は、3番目の例のようなものが必要です。組み合わせる! :
std::chrono::milliseconds duration(1);
while (true)
{
if(!mutex.try_lock())
{
std::this_thread::yield();
std::this_thread::sleep+for(duration);
continue;
}
return;
}
このようなものが保証され、cpuはこの操作が実行されるのと同じ速さで生成されます。また、sleep_for()は、cpuが次の反復を実行する前にしばらく待機することを保証します。この時間はもちろん、動的に(または静的に)ニーズに合わせて調整できます
乾杯:)
必要なのはおそらく条件変数です。条件付きウェイクアップ機能を備えた条件変数は、通常、書き込み中のように実装され、ループ内のスリープまたはyieldは条件を待機します。
コードは次のようになります。
std::unique_lock<std::mutex> lck(mtx)
while(!bSomeCondition) {
cv.wait(lck);
}
または
std::unique_lock<std::mutex> lck(mtx)
cv.wait(lck, [bSomeCondition](){ return !bSomeCondition; })
データの準備ができたら、別のスレッドの条件変数に通知するだけです。ただし、条件変数を使用する場合は、ロックを回避できません。