これらの「ロック」なものへの話

序文

最近という本、読んで:「オペレーティングシステム3つの簡単な小品」の中国語版で、「オペレーティングシステムの概要を、」クレソンの元の本は9.7ポイントを獲得、品質も良いです。周りブック仮想化、並行処理、および持続性オペレーティングシステムの通常の本とは異なり、三つの主要な概念の、展開]ユーモラスしかし鋭い言葉遣い、。これらの日は、私が「ロック」のセクションに焦点を当て、ダウン読んで、より深い理解を持って、いくつかの章を読んで同時。

したがって、この記事では、ロックを解除するために話すの「オペレーティングシステムの概要」の章での研究ノートを、それは助けではなく、それを共有したいと思います。

ロックの基本的な考え方

ロックが実際に変数であり、我々は、次の例のように変数を使用するロックのいくつかのタイプを宣言する必要があります:

lock_t mutex; //声明
...
lock(&mutex); //加锁
balance = balance + 1;
unlock(&mutex); //解锁
复制代码

ロック変数が表される、のいずれか(取得し、又はロックされ、又は保持)占有、ある時点でロック状態を保持し、それはどちらか(avaliable、またはロック解除、またはフリー)が利用可能である、どのスレッドがロックを保持していない示しますそこに、スレッドがロックを保持している重要な地域です。

スケジューリング制御の最小レベルを提供するために、プログラマのためのロックは、スレッドは、プログラマが作成したエンティティとして見られますが、オペレーティングシステムのスケジューラ、オペレーティングシステムの選択によって特定の方法、およびクリティカル領域に追加することによって、いくつかの制御を取得するには、プログラマをロックすることができますロック、一つのスレッドだけが重要な領域の活性であることを確認することができます。

さらに、POSIXライブラリロック呼ばミューテックス(mutexを)スレッド間の相互排他を提供するために使用されるので、すなわちスレッドクリティカルセクションが、それはスレッドが離れて存在する他のスレッドまでのクリティカルセクションに入る防止することができる場合。

ロックを実現する方法

私たちは、プログラマの観点から行っている、ロックがどのように動作するかの一定の理解があります。どのようにそのロックを実装するには?私たちは、ハードウェアのサポートは、何が必要?どのようなオペレーティングシステムでは、サポートを必要としますか?以下は、回答されます。

しかし、達成ロックする前に、我々は明確な目標が必要で、それは「ロック」良い仕事をできるようにするために、いくつかの基準を確立する必要がある、三つの主要な基準があります。

  • 相互に排他的な提供:最も基本的な、ロックは重要なゾーンに入るために、複数のスレッドを防ぐことができるはずです
  • フェアネス:ロックは、それぞれ競合するスレッドがロックをつかむために公平な機会を持っているかどうか、利用できるのですか?
  • パフォーマンス:増加は、時間のオーバーヘッドの後にロックを使用することを検討したいです。たとえば、次のシナリオ:
    • ロックをつかむための唯一つのスレッドの競合状況がなければ、支出のロックを解除する方法?
    • CPUの競争、どのようにパフォーマンス上の複数のスレッド?
    • 複数のCPU、複数のスレッドが競争のパフォーマンス?

Javaへのマッピングされた3つの標準的なレベル、:

  • ミューテックス:同期中にJDKとJUCがロックの最大スレッドが保持していることを確認するために、ロックミューテックスであります
  • フェアロックと非ロックフェア:同時に複数のスレッドがロックを待っていることのJava手段における公正ロック、ロックはロックを適用するために合わせて取得する必要があり、ロックが公平にすることができ、押収し、公正なロックは新しいReentrantLockのを使用することができません達成するために(真)
  • ロック・パフォーマンス:同期ロックのエスカレーションでJDK -偏ったロック、軽量かつヘビー級ロックロック、これらのロックの実装と変換は、同期ロックのパフォーマンスを向上させることです

テストセットコマンド(テスト・アンド・セット命令)

テストセットコマンド(テスト・アンド・セット命令)、x86システム上で、具体的にXCHG(原子交換、原子の交換)を指す命令、我々がテストし、命令セットを定義するために、次のCコードの断片を使用して行います。

int TestAndSet(int *old_ptr,int new) {
    int old = *old_ptr; //fetch old value at old_ptr
    *old_ptr = new; //store 'new' into old_ptr
    return old;  //return the old value
}
复制代码

新しいの新しい値を更新しながら、それは、ポイントold_ptr古い値を返します。また、確かにコンパイル直接従来の使用を示すために、上記の擬似コードは、原子性はオペレーティングシステムのハードウェア命令(必要保証するものではないことに留意されたいXCHGのx86命令アトミック性を保証するために)サポートを。

テスト古い値が、また、新しい値を設定し、両方のため、この命令は、「テストセット」と呼ばれています。このディレクティブに依存することは簡単で実現できるスピンロック(スピンロック)を以下のように、コードは次のとおりです。

typedef struct lock_t {
    int flag;
} lock_t;

void init(lock_t *lock) {
    // 0 表示锁可用,1表示锁已经被抢占
    lock->flag = 0;
}

void lock(lock_t *lock) {
    while (TestAndSet(&lock->flag,1) == 1)
    {
        ; // 自旋等待 (do something)
    }
}

void unlock(lock_t *lock) {
    lock->flag = 0;
}
复制代码

まず、実行中のスレッドを仮定し、(ロックを呼び出す)、他のスレッドがロックを保持していない、ロックを取得するフラグ= 0、(フラグ、1)メソッド呼び出しテスト・アンド・セット、0を返し、whileループのスレッドうち、そう。また、フラグ原子をセットするシンボルロックが保持されている、1です。スレッドがクリティカル領域を離れたとき、0にフラグをクリーンアップするロック解除()を呼び出します。

オペレーティングシステムのハードウェアプリミティブに頼った後、テスト(テスト古いロック値)と(新しい値のセット)を設定する単一のアトミック操作にマージ、我々は一つのスレッドだけがロックを取得できることを保証し、効果的な相互を達成しています除外プリミティブ!

ロックの最も単純な種類のコード上でスピンロック(スピンロック)実装、ロックが利用可能になるまで、スピン、使用のCPUサイクルでした。さて、基本的なスピンロックを評価するために、以前の基準に従って:

  • 相互に排他的なことができます。スピンロックはロックの正確さを保証するために、クリティカルセクションを入力するだけで一つのスレッドを許可します
  • 公平性:スピンロックが保証フェアネス・タスクを提供していない、実際には、競争条件の下で、スレッドのスピンを回転することができるかもしれません。スピンロックが公平ではない、それは飢餓につながる可能性があります。
  • パフォーマンスの問題は:スピンロックのために、単一CPUの場合には、パフォーマンスがオーバーヘッドが非常に大きい場合、CPUを放棄する前に、ロックを競合する複数のスレッドは、時間のスライス、CPUサイクルの無駄を回転します。、スレッドAは、スレッドロックとBのCPU2競争、CPU1で想定されている(スレッドの数が実質的にCPUの数に等しい場合)、マルチCPUは、スピンロック性能が良好です。A(CPU1)ロックの所有権をスレッド、ロックが(CPU2上)スレッドBの競争を回転します。しかし、重要な領域はすぐにロックすることができますので、その後、ロックを獲得するためにBをスレッド、非常に短く、一般的に

比較とスワップ(比較交換)

いくつかのシステム、すなわち、比較およびスワップ命令(別のハードウェア・プリミティブを提供するSPARCシステムは、コンペアアンドスワップ、x86システムのコンペアと交換される)、次のC言語の擬似コード命令ということです。

int CompareAndSwap(int *ptr,int expected,int new) {
    int actual = *ptr;
    if (actual == expected)
    {
        *ptr = new;
    }
    return actual;
}
复制代码

比較すると基本的な考え方の交換期待値が等しいPTRによって指されるかどうかを検出することである。その場合、更新は新しい値PTRと呼ばれます。そうでなければ、何もしません。メモリアドレスのいずれかの場合には、戻り値

比較およびスワップ命令と、ロックは、テストセットとして使用したのと同様に達成することができます。例えば、我々は唯一のロック()関数を置き換えるために、次のコードを使用する必要があります。

void lock(lock_t *lock) {
    while (CompareAndSwap(&lock->flag,0,1) == 1)
        ; //spin    
}
复制代码

比較とスワップ命令が実際にあるCASの Java開発作業ではコマンド、私たちはしばしば、このような原子クラスAtomicXXXの使用など、遭遇する、内部実装はCAS操作を使用することです。

入手して増加(フェッチおよび加算)

ハードウェアのプリミティブがあります:取得し、増加(アドオンをフェッチし、-)命令は、それはアトミックに、特定のアドレスの古い値を返し、値がx86システムでは1つインクリメントさせ、XADD命令です。次のように増加したC言語の擬似コードを取得します。

int FetchAndAdd(int *ptr) {
    int old = *ptr;
    *ptr = old + 1;
    return old;
}

typedef struct lock_t {
    int ticket;
    int turn;
} lock_t;

void lock_init(lock_t *lock) {
    lock->ticket = 0;
    lock->turn = 0;
}

void lock(lock_t *lock) {
    int myturn = FetchAndAdd(&lock->ticket);
    while (lock->turn != myturn) 
        ;//spin
}
void unlock(lock_t *lock) {
    FetchAndAdd(&lock->turn);
}
复制代码

この例では、我々は命令の実装を取得し、増やすチケットロックが実際に公正なロックです。

これは、値を使用することによって達成されていませんが、二つの変数チケットやターンの使用が構築しました。基本的な考え方は次のとおりです。あなたがスレッドロックを取得したい場合は、命令を取得し、増加する原子のチケット値の最初の実装。「ターン」のスレッドとしてこの値(すなわちmyturnは、スレッドセットの順序のためにロックを取得します)。グローバルに共有ロックイン>ターン変数、1スレッドmyturn ==ターンよると、糸のターンは、クリティカルセクションに入ります。ロック解除は、次の待機スレッドがクリティカル領域に入ることができるので、ターンを高めることです。

これは、メソッドのすべてのスレッドがロックを取得できることを保証し、そして順に、スレッドに基づいており、また言って呼ばれている最初のうちの最初の種類(FIFO)公平性メカニズム、ロックでアレンジスピンロックは(チケットロック)

過度のスピンを回避する方法

いくつかの命令は、その実装は非常に簡単です、スピンロックの前に達成することができますが、あまりにも多くのスピンは、どのようにそれを行うには?例えば、2つのスレッドが、スレッド(スレッド0)はロックを保持している場合、中断され、例えば、単一のプロセッサ上で実行します。ロックを取得するための第2のスレッド(スレッド1)、ロックを保持することが見出されています。したがって、それはスピンし、スピンを開始します。長い時間のためのスレッド0は、ロックを保持している場合、スレッド2は常に不必要なスピンロックを軽減するので、どのように、CPU時間を無駄に、スピンのだろうか?

唯一のハードウェアサポートが十分ではありません、我々はまた、オペレーティングシステムのサポートを必要とします!

使用キュー:オルタナティブスピンスリープ

我々は使用することができ、Solarisの 2つのコールを提供してサポートし、:

  • 公園は()、睡眠への呼び出し元のスレッドを可能に
  • unparkを(スレッドID)は、スレッドIDを特定したスレッドを起動します

どちらの呼び出しが可能な場合、ロックに目覚め、呼び出し側がロック睡眠を得ることができないように、ロックを達成することができます。以下は、C言語の擬似コードです。

typedef struct lock_t {
    int flag;
    int guard;
    queue_t *q;
} lock_t;

void lock_init(lock_t *m) {
    m->flag = 0;
    m->guard = 0;
    queueu_init(m->q);
}

void lock(lock_t *m) {
    while (TestAndSet(&m->guard,1) == 1)
       ; //acquire guard lock by spinning
    if (m->flag == 0) {
        m->flag = 1; //lock is acquired
        m->guard = 0;
    } else {
        queue_add(m->q,gettid());
        m->guard = 0;
        park();
    }
}

void unlock(lock_t *m) {
    while (TestAndSet(&m->guard,1) == 1)
        ; // acquire guard lock by spinning
    if (queue_empty(m->q)) {
        m->flag = 0; // let go of lock;on one wants it;
    } else {
        unpark(queue_remove(m->q)) ;//hold lock(for next thread!)
    }
    m->guard = 0;
}
复制代码

私たちは、それが実質的にガードスピンロックを働き、前回のテストと待ちキューを設定し、ロックの高い性能を達成するために結合します。第二に、我々は飢餓を避けるために、ロックを取得するためにキューを介して人をコントロールする必要があります。

スピンロックでのJava

スピン待つ長い時間がプロセッサ時間を消費します。この点で、スピンでJavaは特定のアクションがあるロック:スピン待ち時間が一定の限度を持っている必要があり、スピンロックを取得するために、成功せず、限られた回数を超えた場合、スレッドが中断されなければならない、10にデフォルトの数を制限変化にPreBlockSpin:時間は、あなたは-XXを使用することができます。

導入におけるJDK1.4.2スピンロックは、使用-XX:+開くためにUseSpinningは、その実装原理は、CASの前に言及されています。JDK 6はデフォルトで有効になっており、適応スピンロック(適応スピンロック)の導入となります。

時間(数)のスピンがもはや固定されているが、スピンロック時間た状態でロックされ、所有者が以前によって決定される適応手段。同じオブジェクトスピンウェイトロックを保持しているだけで、正常優勝ロック、およびスレッドのロックが実行されている場合、仮想マシンは、このスピンが再び成功する可能性が高いと思いますし、それがスピンを許可します時間の比較的長い期間を待ちます

参考資料

おすすめ

転載: juejin.im/post/5e2ed6ec6fb9a0300052c12d