スピンロックと反発ロック

6181 ワード

変換元:http://www.csdn123.com/html/blogs/20130509/11141.htm スピンロックは、データがロックされていない場合はロックを取得して実行し、データがロックされている場合は、単一プロセッサ環境(非プリエンプトカーネル)でスピンロックが単一プロセッサプリエンプトカーネルとして機能しない場合、スピンロックがプリエンプトを禁止する役割を果たすスピンロックはSMPアーキテクチャにおけるlow-levelの同期機構である.スレッドAがスピンロックを取得しようとすると、そのロックが他のスレッドによってロックされる場合、スレッドAは、ロックが利用可能かどうかを検出するために1サイクルでスピンする.オートロックについては、スピン時にCPUを解放しないため、スピンロックを持つスレッドはできるだけ早くスピンロックを解放しなければならない.そうしないと、スピンロックを待つスレッドはずっとそこでスピンし、CPU時間を浪費する.スピンロックを持つスレッドは、sleepの前にスピンロックを解放して、他のスレッドがスピンロックを得ることができるようにしなければならない.(カーネルプログラミングでは、スピンロックのコードsleepを持っているとシステム全体が停止する可能性があります.最近、カーネルの問題を解決したばかりのの問題は、スピンロックを持っている間にsleepが発生し、すべてのコアがすべて停止(8コアのCPU)することになります.どのロックを使用してもシステムリソース(メモリリソースとCPU時間)を消費する必要があります.このリソースの消費量は、次の2つに分類されます.
ロックの確立に必要なリソーススレッドがブロックされたときにロックに必要なリソースPthreadsが提供するSpin Lockロック操作に関連するAPIは主に以下の通りである.
    int pthread_spin_destroy(pthread_spinlock_t *);
    int pthread_spin_init(pthread_spinlock_t *, int);
    int pthread_spin_lock(pthread_spinlock_t *);
    int pthread_spin_trylock(pthread_spinlock_t *);
    int pthread_spin_unlock(pthread_spinlock_t *);

1)スピンロックの初期化
pthread_spin_Initは、スピンロックの使用に必要なリソースを申請し、非ロック状態に初期化するために使用される.psharedの値とその意味:
PTHREAD_PROCESS_SHARED:このスピンロックは、複数のプロセスのスレッド間で共有できます.PTHREAD_PROCESS_PRIVATE:このスピンロックは、本スピンロックのスレッドが存在するプロセス内のスレッドのみを初期化して使用できます.2)スピンロックpthread_を得るspin_lockは指定したスピンロックを取得(ロック)するために使用されます.スピンロックが現在他のスレッドに所有されていない場合、関数を呼び出すスレッドはスピンロックを取得します.そうしないと、関数はスピンロックを取得する前に戻りません.関数を呼び出すスレッドが関数を呼び出すときにスピンロックを持っている場合、結果は不確定です.
3)スピンロックpthread_を取得しようとするspin_trylockは指定したスピンロックを取得しようとしますが、取得できない場合は戻りに失敗します.
4)スピンロックpthread_を解放(ロック解除)するspin_unlockは、指定されたスピンロックを解放するために使用されます.
5)スピンロックを破棄するpthread_spin_destroyは、指定されたスピンロックを破棄し、関連するすべてのリソース(いわゆるpthread_spin_initによって自動的に申請されたすべてのリソース)を解放するために使用されます.この関数が呼び出された後、pthread_spin_Initがスピンロックを再初期化すると、ロックを使用しようとした呼び出しの結果は定義されません.この関数が呼び出されたときにスピンロックが使用されているか、スピンロックが初期化されていない場合は、結果は定義されません.反発量とスピンロックの違い:Pthreadsが提供するMutexロック操作に関するAPIは主に以下の通りである.
    pthread_mutex_lock (pthread_mutex_t *mutex);
    pthread_mutex_trylock (pthread_mutex_t *mutex);
    pthread_mutex_unlock (pthread_mutex_t *mutex);
    Pthreads    Spin Lock      API   :
    pthread_spin_lock (pthread_spinlock_t *lock);
    pthread_spin_trylock (pthread_spinlock_t *lock);
    pthread_spin_unlock (pthread_spinlock_t *lock);

実現原理から言えば、Mutexはsleep-waitingタイプのロックに属する.例えば、1つのデュアルコアマシンには、Core 0およびCore 1上でそれぞれ動作する2つのスレッド(スレッドAおよびスレッドB)がある.スレッドAがpthread_を通過したいと仮定するmutex_ロック操作は、臨界領域のロックを取得し、このロックがスレッドBによって保持されている場合、スレッドAはブロックされ、Core 0は、コンテキスト切替(Context Switch)を行い、スレッドAを待機キューに配置し、Core 0は、他のタスク(例えば、別のスレッドC)を実行して待つ必要がなくてもよい.Spin lockはそうではなくbusy-waitingタイプのロックに属し、スレッドAがpthread_を使用している場合spin_ロック操作によってロックが要求されると、スレッドAは、このロックが得られるまでCore 0上で待機しながらロック要求を行う.Linux glibcにおけるpthreads APIの実装NPTL(Native POSIX Thread LibraryネイティブPOSIXスレッドライブラリ)のソースコードを調べると(「getconf GNU_LIBPTHREAD_VERSION」コマンドを使用して、我々のシステムにおけるNPTLのバージョン番号が得られる)、pthread_mutex_ロック()操作ロックが成功しなければsystem_を呼び出すwait()のシステム呼び出しは、現在のスレッドをmutexの待機キューに追加します.一方、spin lockは、1つのwhile(1)サイクルで埋め込まれたアセンブリコードで実現されるロック操作と理解できる(linuxカーネルではspin lock操作は2つのCPU命令しか必要なく、ロック解除操作は1つの命令で完了できるという論文を見たことがある).興味のある方はsanosというもう一つのマイクロカーネルのpthreds APIの実装:mutex.c spinlock.cを参考にすることができます.NPTLのコード実装とは異なりますが、その実装は非常に簡単で分かりやすいので、spin lockとmutexの特性を理解するのに役立ちます.スピンロックにとって、ロックを確立するために少ないリソースを消費するだけです.その後、スレッドがブロックされると、ロックが使用可能かどうかを繰り返しチェックします.すなわち、スピンロックが待機状態にある場合、CPU時間が消費されます.反発ロックにとって、スピンロックに比べてロックを確立するために大量のシステムリソースを消費する必要がある.その後、スレッドがブロックされると、スレッドのスケジューリング状態が変更され、スレッドが待機スレッドキューに追加される.最後に、ロックが使用可能になると、ロックを取得する前に、スレッドは待機キューから取り出され、スケジューリングステータスが変更されます.しかし、スレッドがブロックされている間、CPUリソースは消費されません.したがって、スピンロックと反発ロックは、異なるシーンに適しています.スピンロックは、短い時間だけブロックするシーンに適用され、反発ロックは、長い時間ブロックする可能性があるシーンに適用されます.
スピンロックとlinuxカーネルプロセスのスケジューリング関係臨界領域に睡眠を引き起こすコードが含まれる可能性がある場合は、スピンロックは使用できません.そうしないと、デッドロックを引き起こす可能性があります.では、なぜ信号量保護のコードは睡眠でき、スピンロックはできないのでしょうか.まずスピンロックの実現方法を見てみましょう.スピンロックの基本的な形式は以下の通りです.
  spin_lock(&mr_lock);
  //   
  spin_unlock(&mr_lock);
      spin_lock(&mr_lock)   
  #define spin_lock(lock) _spin_lock(lock)
  #define _spin_lock(lock) __LOCK(lock)
  #define __LOCK(lock) \
  do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)

「preempt_disable()」に注意してください.この呼び出しの機能は「プリエンプトを閉じる」です(spin_unlockでプリエンプト機能が再開されます).ここから、スピンロックによって保護された領域は、非占有状態で動作することが分かる.ロックが取得されなくても、[スピン](Spin)状態ではプリエンプトは禁止されます.これを知って、スピンロック保護のコードが睡眠できない理由を理解できると思います.考えてみてください.スピンロック保護のコードの間にスリープが発生し、プロセススケジューリングが発生すると、別のプロセスがspinlock保護のコードを再び呼び出す可能性があります.ロックが取得されない「スピン」状態でも、プリエンプトは禁止されていることがわかりましたが、「スピン」は動的で、もう睡眠が取れません.つまり、このプロセッサではプロセススケジューリングが発生しないので、デッドロックは自然に発生します.
スピンロックの特徴をまとめることができます:●単一プロセッサの非プリエンプトコアの下:スピンロックはコンパイル時に無視されます;●シングルプロセッサプリエンプトカーネルの下:スピンロックはカーネルプリエンプトを設定するスイッチとしてのみ使用する;●マルチプロセッサの下:この時こそ完全にスピンロックの作用を発揮することができ、スピンロックはカーネルの中で主にマルチプロセッサの中で臨界領域への同時アクセスを防止し、カーネルの占有による競争を防止するために用いられる.
linuxプリエンプトが発生した時間最後に、linuxプリエンプトが発生した時間を理解し、プリエンプトはユーザープリエンプトとカーネルプリエンプトに分けられます.ユーザプリエンプトは、●システム呼び出しからユーザ空間に戻る●割り込みハンドラからユーザ空間に戻る
  • カーネルプリエンプトは、割り込み処理プログラムからカーネル空間を返すと、カーネルがプリエンプト可能である場合に発生する.・カーネルコードが再びプリエンプト可能である場合.(例:spin_unlockの場合)●カーネル内のタスクが明示的にschedule()を呼び出す場合●カーネル内のタスクがブロックされている場合.基本的なプロセススケジューリングは、クロックが中断された後に発生し、プロセスのタイムスライスが使用済みであることを発見すると、プロセスプリエンプトが発生する.通常、割り込みハンドラがカーネル空間に戻るときにカーネルプリエンプトを行うことで、I/Oイベントが発生したときに対応する割り込みハンドラがアクティブになり、このI/Oイベントを待っているプロセスがあることを発見すると、待機プロセスがアクティブになり、現在実行中のプロセスのneed_が設定されます.reschedフラグは、割り込み処理プログラムが戻ってくると、スケジューラがアクティブになり、I/Oイベントを待つプロセスで実行権が得られ、I/Oイベントに対する相対的な高速応答(ミリ秒レベル)が保証される.I/Oイベントが発生すると,I/Oイベントの処理プロセスが現在のプロセスを占有し,システムの応答速度はスケジューリングタイムスライスの長さとは無関係であることがわかる.

  • まとめ:(1)Mutexはロック操作が非常に頻繁なシーンに適しており,より適応性が高い.spin lockよりもコストがかかります(主にコンテキスト切替です).しかし、実際の開発で複雑なアプリケーションシーンに適しており、一定のパフォーマンスを保証する前提でより柔軟性を提供します.
    (2)spin lockのlock/unlockは性能が優れている(より少ないcpu命令がかかる)が,臨界領域の動作時間が短いシーンにのみ適応する.実際のソフトウェア開発では、プログラマーが自分のプログラムのロック動作をよく知らない限り、spin lockを使用するのは良いアイデアではありません(通常、1つのマルチスレッドプログラムではロックの操作が数万回あり、失敗したロック操作(contended lock requests)が多すぎると、多くの時間を無駄にして空待機します).
    (3)より安全な方法は,まず(保守的に)Mutexを用い,その後性能にさらなる需要があればspin lockを用いてチューニングを試みることができるかもしれない.結局、私たちのプログラムはLinux kernelのように性能に対する需要ほど高くありません(Linux Kernelで最もよく使われるロック操作はspin lockとrw lockです).