スピンロックを使用して、非常に小さなクリティカルセクションを保護しています。競合が発生するのは非常にめったにないため、通常のミューテックスよりもスピンロックの方が適しています。
私の現在のコードは次のとおりであり、x86とGCCを想定しています。
volatile int exclusion = 0;
void lock() {
while (__sync_lock_test_and_set(&exclusion, 1)) {
// Do nothing. This GCC builtin instruction
// ensures memory barrier.
}
}
void unlock() {
__sync_synchronize(); // Memory barrier.
exclusion = 0;
}
だから私は思っています:
__sync_lock_release
もあります。私はメモリバリアの専門家ではないので、__sync_synchronize
の代わりにこれを使用してもよいかどうかはわかりません。競合についてはまったく気にしません。数日ごとに1回、スピンロックをロックしようとする他のスレッドが1つ、おそらく2つあります。
だから私は思っています:
* Is it correct?
言及された文脈では、私はイエスと言うでしょう。
* Is it optimal?
それはロードされた質問です。ホイールを再発明することにより、他の実装によって解決された多くの問題を再発明することにもなります
ロックワードにアクセスしようとしていない場合、失敗時に無駄なループが発生すると思います。
ロック解除で完全なバリアを使用するには、リリースセマンティクスのみが必要です(そのため、__ sync_lock_releaseを使用して、mfの代わりにitaniumでst1.relを取得するか、またはpowerpcでlwsyncを取得します...)。本当にx86またはx86_64のみに関心がある場合、ここで使用されるバリアのタイプはそれほど重要ではありません(ただし、HP-IPFポート用のインテルのイタニウムにジャンプする場所がある場合、これは必要ありません)。
無駄ループの前に通常置くpause()命令はありません。
必要な競合があるとき何か、semop、または必死の愚かな眠り。あなたが本当にあなたが買うパフォーマンスを必要とするならば、futexの提案はおそらく良いものです。パフォーマンスが必要な場合は、このコードを使用して、このコードをmaintainできます。
リリースバリアは必要ないというコメントがあったことに注意してください。これは、x86でも当てはまりません。リリースバリアは、「バリア」の周りの他のメモリアクセスをシャッフルしないようにするコンパイラへの命令としても機能するためです。 asm( "" ::: "memory")を使用した場合に得られるものと非常に似ています。
* on compare and swap
X86では、sync_lock_test_and_setは暗黙のロックプレフィックスを持つxchg命令にマップされます。間違いなく最もコンパクトに生成されたコード(特に、intの代わりに "lock Word"にバイトを使用する場合)。ただし、LOCK CMPXCHGを使用する場合よりも正確です。比較とスワップの使用は、より洗練されたアルゴリズムに使用できます(失敗時に最初の「待機者」のメタデータへのゼロ以外のポインターをロックワードに入れるようなもの)。
私には元気に見えます。ところで、これは競合する場合でもより効率的な textbook 実装です。
void lock(volatile int *exclusion)
{
while (__sync_lock_test_and_set(exclusion, 1))
while (*exclusion)
;
}
あなたの質問に答えて:
__sync_lock_release()
の場合はunlock()
を使用する方が少し良いかもしれません。これはロックをデクリメントし、1回の操作でメモリバリアを追加するためです。ただし、競合が発生することはほとんどないというあなたの主張を前提としています。それは私にはよさそうだ。Linuxの最新バージョンを使用している場合は、 futex -"高速ユーザー空間ミューテックス"を使用できる場合があります。
適切にプログラムされたfutexベースのロックは、ロックが競合している場合を除いて、システムコールを使用しません。
スピンロックで最適化しようとしている、競合しないケースでは、futexは、カーネルのsyscallを必要とせずに、スピンロックのように動作します。ロックが競合する場合、待機はビジー待機なしでカーネルで行われます。
次のCAS実装はx86_64での正しい実装かどうかと思います。私のi7 X920ラップトップ(Fedora 13 x86_64、gcc 4.4.5)では、ほぼ2倍高速です。
inline void lock(volatile int *locked) {
while (__sync_val_compare_and_swap(locked, 0, 1));
asm volatile("lfence" ::: "memory");
}
inline void unlock(volatile int *locked) {
*locked=0;
asm volatile("sfence" ::: "memory");
}
正しさについてコメントすることはできませんが、質問の本文を読む前に、質問のタイトルが赤信号になりました。同期プリミティブは、正確さを保証するのが途方もなく困難です...可能であれば、おそらく pthreads または boost :: thread =。
いくつかの間違った仮定があります。
まず、SpinLockは、リソースが別のCPUでロックされている場合にのみ意味があります。同じCPUでリソースがロックされている場合(ユニプロセッサーシステムでは常にそうです)、リソースをロック解除するには、スケジューラーを緩和する必要があります。スケジューラは自動的にタスクを切り替えるため、現在のコードはユニプロセッサシステムで機能しますが、リソースの浪費です。
マルチプロセッサシステムでも同じことが起こりますが、タスクが1つのCPUから別のCPUに移行する場合があります。つまり、タスクが別のCPUで実行されることが保証されている場合は、スピンロックの使用が適切です。
次に、ミューテックスのロックIS高速(スピンロックと同じくらい高速)がロック解除されている場合。ミューテックスのロック(およびロック解除)は、ミューテックスが既にロックされている場合にのみ低速(非常に低速)です。
したがって、あなたの場合、ミューテックスを使用することをお勧めします。
[〜#〜] tatas [〜#〜] (test-and-test-and-set)を使用することをお勧めします。 CAS演算の使用はプロセッサにとって非常に高価であると考えられているため、可能であればそれらを回避することをお勧めします。もう1つ、優先順位の逆転の影響を受けないようにしてください(優先順位の高いスレッドがロックを取得しようとしたときに、優先順位の低いスレッドがロックを解放しようとした場合はどうなりますか?たとえば、Windowsでは、この問題は最終的に次のように解決されます。スケジューラーは優先度ブーストを使用しますが、最後の20回の試行でロックの取得に成功しなかった場合に備えて、スレッドのタイムスライスを明示的にあきらめることができます(例..)
ロック解除手順にはメモリバリアは必要ありません。除外への割り当ては、x86で整列されている限り、アトミックです。
X86(32/64)の特定のケースでは、ロック解除コードにメモリフェンスはまったく必要ないと思います。 x86は、ストアが最初にストアバッファーに入れられ、他のスレッドが可視になるのを遅らせることができることを除いて、並べ替えを行いません。また、ストアしてから同じ変数から読み取るスレッドは、まだメモリにフラッシュされていない場合、ストアバッファーから読み取ります。したがって、必要なのは、コンパイラの順序変更を防ぐためのasm
ステートメントだけです。他のスレッドの観点からは、1つのスレッドがロックを必要以上に少し長く保持するリスクがありますが、競合を気にしない場合は、問題になりません。実際、pthread_spin_unlock
は私のシステム(linux x86_64)のように実装されています。
私のシステムもxchg
を使用する代わりにpthread_spin_lock
を使用してlock decl lockvar; jne spinloop;
を実装しています(__sync_lock_test_and_set
が使用するものです)が、実際にパフォーマンスの違いがあるかどうかはわかりません。