web-dev-qa-db-ja.com

スピンロックの実装は正しく最適ですか?

スピンロックを使用して、非常に小さなクリティカルセクションを保護しています。競合が発生するのは非常にめったにないため、通常のミューテックスよりもスピンロックの方が適しています。

私の現在のコードは次のとおりであり、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;
}

だから私は思っています:

  • このコードは正しいですか?相互排除は正しく行われていますか?
  • すべてのx86オペレーティングシステムで動作しますか?
  • X86_64でも動作しますか?すべてのオペレーティングシステムで?
  • 最適ですか?
    • Compare-and-Swapを使用したスピンロックの実装を見たことがありますが、どちらが良いかわかりません。
    • GCCのアトミック組み込みドキュメント( http://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Atomic-Builtins.html )によると、__sync_lock_releaseもあります。私はメモリバリアの専門家ではないので、__sync_synchronizeの代わりにこれを使用してもよいかどうかはわかりません。
    • 競合がない場合に最適化しています。

競合についてはまったく気にしません。数ごとに1回、スピンロックをロックしようとする他のスレッドが1つ、おそらく2つあります。

38
Hongli

だから私は思っています:

* 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を使用する場合よりも正確です。比較とスワップの使用は、より洗練されたアルゴリズムに使用できます(失敗時に最初の「待機者」のメタデータへのゼロ以外のポインターをロックワードに入れるようなもの)。

18
Peeter Joot

私には元気に見えます。ところで、これは競合する場合でもより効率的な textbook 実装です。

void lock(volatile int *exclusion)
{
    while (__sync_lock_test_and_set(exclusion, 1))
        while (*exclusion)
            ;
}
21
sigjuice

あなたの質問に答えて:

  1. 私には大丈夫に見えます
  2. OSがGCCをサポートしている(そしてGCCが機能を実装している)と仮定します。これはすべてのx86オペレーティングシステムで動作するはずです。 GCCのドキュメントでは、特定のプラットフォームでサポートされていない場合は警告が表示されることを示しています。
  3. ここにはx86-64固有のものはないので、なぜそうなのかわかりません。これは、GCCがサポートするanyアーキテクチャをカバーするように拡張できますが、非x86アーキテクチャでこれを実現するためのより最適な方法が存在する可能性があります。
  4. __sync_lock_release()の場合はunlock()を使用する方が少し良いかもしれません。これはロックをデクリメントし、1回の操作でメモリバリアを追加するためです。ただし、競合が発生することはほとんどないというあなたの主張を前提としています。それは私にはよさそうだ。
4
DaveR

Linuxの最新バージョンを使用している場合は、 futex -"高速ユーザー空間ミューテックス"を使用できる場合があります。

適切にプログラムされたfutexベースのロックは、ロックが競合している場合を除いて、システムコールを使用しません。

スピンロックで最適化しようとしている、競合しないケースでは、futexは、カーネルのsyscallを必要とせずに、スピンロックのように動作します。ロックが競合する場合、待機はビジー待機なしでカーネルで行われます。

3

次の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");
}
3
Alex Raybosh

正しさについてコメントすることはできませんが、質問の本文を読む前に、質問のタイトルが赤信号になりました。同期プリミティブは、正確さを保証するのが途方もなく困難です...可能であれば、おそらく pthreads または boost :: thread =。

2
Jason S

いくつかの間違った仮定があります。

まず、SpinLockは、リソースが別のCPUでロックされている場合にのみ意味があります。同じCPUでリソースがロックされている場合(ユニプロセッサーシステムでは常にそうです)、リソースをロック解除するには、スケジューラーを緩和する必要があります。スケジューラは自動的にタスクを切り替えるため、現在のコードはユニプロセッサシステムで機能しますが、リソースの浪費です。

マルチプロセッサシステムでも同じことが起こりますが、タスクが1つのCPUから別のCPUに移行する場合があります。つまり、タスクが別のCPUで実行されることが保証されている場合は、スピンロックの使用が適切です。

次に、ミューテックスのロックIS高速(スピンロックと同じくらい高速)がロック解除されている場合。ミューテックスのロック(およびロック解除)は、ミューテックスが既にロックされている場合にのみ低速(非常に低速)です。

したがって、あなたの場合、ミューテックスを使用することをお勧めします。

0

[〜#〜] tatas [〜#〜] (test-and-test-and-set)を使用することをお勧めします。 CAS演算の使用はプロセッサにとって非常に高価であると考えられているため、可能であればそれらを回避することをお勧めします。もう1つ、優先順位の逆転の影響を受けないようにしてください(優先順位の高いスレッドがロックを取得しようとしたときに、優先順位の低いスレッドがロックを解放しようとした場合はどうなりますか?たとえば、Windowsでは、この問題は最終的に次のように解決されます。スケジューラーは優先度ブーストを使用しますが、最後の20回の試行でロックの取得に成功しなかった場合に備えて、スレッドのタイムスライスを明示的にあきらめることができます(例..)

0
unknown

ロック解除手順にはメモリバリアは必要ありません。除外への割り当ては、x86で整列されている限り、アトミックです。

0
Ira Baxter

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が使用するものです)が、実際にパフォーマンスの違いがあるかどうかはわかりません。

0
JanKanis