web-dev-qa-db-ja.com

一般的な再入可能ロックと概念とは何ですか?

私はいつも混乱します。誰かが リエントラント が異なるコンテキストで何を意味するのか説明してくれますか?そして、なぜ再入可能と非再入可能を使用したいのですか?

Pthread(posix)のロックプリミティブと言ってください。それらは再入可能ですか?それらを使用するとき、どのような落とし穴を避ける必要がありますか?

ミューテックスは再入可能ですか?

81
vehomzzz

再入可能ロック

再入可能ロックは、プロセスがそれ自体をブロックせずに複数回ロックを要求できるロックです。既にロックを取得しているかどうかを追跡するのが簡単ではない状況で役立ちます。ロックがリエントラントでない場合は、ロックを取得し、再度取得するときにブロックして、効果的に独自のプロセスをデッドロックすることができます。

一般に再入可能性は、コードのプロパティであり、実行中にコードが呼び出された場合に破損する可能性のある中央の変更可能な状態はありません。このような呼び出しは、別のスレッドによって行うことも、コード自体から始まる実行パスによって再帰的に行うこともできます。

コードが実行中に更新される可能性のある共有状態に依存している場合、少なくともその更新によって破損される可能性がある場合を除き、コードは再入可能ではありません。

再入可能ロックの使用例

リエントラントロックのアプリケーションの(やや汎用的で不自然な)例は次のとおりです。

  • グラフをたどるアルゴリズム(おそらくサイクルが含まれるアルゴリズム)を含む計算があります。トラバーサルは、サイクルまたは同じノードへの複数のパスのために、同じノードを複数回訪問する場合があります。

  • データ構造は同時アクセスの対象であり、何らかの理由で、おそらく別のスレッドによって更新される可能性があります。個々のノードをロックして、競合状態による潜在的なデータ破損に対処できる必要があります。何らかの理由(おそらくパフォーマンス)で、データ構造全体をグローバルにロックしたくありません。

  • 計算では、訪問したノードの完全な情報を保持できません。または、「ここに来たことがありますか」という質問にすばやく回答できないデータ構造を使用しています。

    この状況の一例は、バイナリヒープとして実装された優先度キューを使用したダイクストラのアルゴリズムの単純な実装、またはキューとして単純なリンクリストを使用した幅優先検索です。これらの場合、既存の挿入のキューをスキャンすることはO(N)であり、すべての反復で実行したくない場合があります。

この状況では、すでに取得したロックを追跡するのは高価です。ノードレベルでロックを実行すると仮定すると、再入可能なロックメカニズムにより、以前にノードにアクセスしたことがあるかどうかを判断する必要がなくなります。ノードを盲目的にロックするだけで、キューからポップした後にロックを解除することができます。

再入可能ミューテックス

単純なミューテックスはリエントラントではありません。特定の時点でクリティカルセクションに配置できるスレッドは1つだけです。ミューテックスを取得してから再度取得しようとすると、単純なミューテックスには、以前に誰がそれを保持していたかを知るのに十分な情報がありません。これを再帰的に行うには、各スレッドにトークンがあり、誰がミューテックスを取得したかを判別できるメカニズムが必要です。これにより、ミューテックスメカニズムのコストがいくらか高くなるため、すべての状況でミューテックスメカニズムを実行する必要はありません。

IIRC POSIXスレッドAPIは、再入可能および非再入可能ミューテックスのオプションを提供します。

再入可能ロックを使用すると、リソースMにロックを設定するメソッドAを記述し、Mを再帰的に呼び出すか、Aに既にロックを保持しているコードから呼び出すことができます。

再入不可ロックでは、2つのバージョンのMが必要になります。1つはロックし、もう1つはロックせず、適切なものを呼び出すための追加のロジックです。

19
Henk Holterman

リエントラントロックは、この チュートリアル で非常によく説明されています。

チュートリアルの例は、グラフのトラバースに関する回答の場合よりもはるかに不自然です。再入可能ロックは、非常に単純な場合に役立ちます。

13
Ratna Beresford

再帰ミューテックスの理由と理由は、受け入れられた回答に記載されているような複雑なものであってはなりません。

ネットを掘り下げた後、私の理解を書き留めたいと思います。


最初に、mutexについて話すとき、マルチスレッドの概念も間違いなく関与していることを理解する必要があります。 (mutexは同期に使用されます。プログラムに1つのスレッドしかない場合、mutexは必要ありません)


次に、 normal mutex recursive mutex の間の違いを知っておく必要があります。

[〜#〜] apue [〜#〜]から引用

(再帰的ミューテックスはa)同じスレッドが最初にロックを解除せずに複数回ロックできるミューテックスタイプ。

重要な違いは、同じスレッド内で、再帰ロックを再ロックしてもデッドロックにならず、スレッドをブロックしないことです。

これは、拒否ロックがデッドロックを引き起こさないことを意味しますか?
いいえ、ロックを解除せずに1つのスレッドでロックし、他のスレッドでロックしようとすると、通常のミューテックスとしてデッドロックを引き起こす可能性があります。

いくつかのコードを証明として見てみましょう。

  1. デッドロックのある通常のミューテックス
_#include <pthread.h>
#include <stdio.h>

pthread_mutex_t lock;


void * func1(void *arg){
    printf("thread1\n");
    pthread_mutex_lock(&lock);
    printf("thread1 hey hey\n");

}


void * func2(void *arg){
    printf("thread2\n");
    pthread_mutex_lock(&lock);
    printf("thread2 hey hey\n");
}

int main(){
    pthread_mutexattr_t lock_attr;
    int error;
//    error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_RECURSIVE);
    error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_DEFAULT);
    if(error){
        perror(NULL);
    }

    pthread_mutex_init(&lock, &lock_attr);

    pthread_t t1, t2;

    pthread_create(&t1, NULL, func1, NULL);
    pthread_create(&t2, NULL, func2, NULL);

    pthread_join(t2, NULL);

}
_

出力:

_thread1
thread1 hey hey
thread2
_

一般的なデッドロックの例、問題ありません。

  1. デッドロックのある再帰的ミューテックス

この行のコメントを外してください
error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_RECURSIVE);
そしてもう一方をコメントアウトします。

出力:

_thread1
thread1 hey hey
thread2
_

はい、再帰的ミューテックスもデッドロックを引き起こす可能性があります。

  1. 通常のミューテックス、同じスレッドで再ロック
_#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

pthread_mutex_t lock;


void func3(){
    printf("func3\n");
    pthread_mutex_lock(&lock);
    printf("func3 hey hey\n");
}

void * func1(void *arg){
    printf("thread1\n");
    pthread_mutex_lock(&lock);
    func3();
    printf("thread1 hey hey\n");

}


void * func2(void *arg){
    printf("thread2\n");
    pthread_mutex_lock(&lock);
    printf("thread2 hey hey\n");
}

int main(){
    pthread_mutexattr_t lock_attr;
    int error;
//    error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_RECURSIVE);
    error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_DEFAULT);
    if(error){
        perror(NULL);
    }

    pthread_mutex_init(&lock, &lock_attr);

    pthread_t t1, t2;

    pthread_create(&t1, NULL, func1, NULL);
    sleep(2); 
    pthread_create(&t2, NULL, func2, NULL);

    pthread_join(t2, NULL);

}
_

出力:

_thread1
func3
thread2
_

_thread t1_、_func3_のデッドロック。
sleep(2)を使用して、最初に_func3_での再ロックによってデッドロックが引き起こされることを確認しやすくします)

  1. 再帰的ミューテックス、同じスレッドで再ロック

繰り返しますが、再帰的なmutex行のコメントを外し、他の行をコメント化します。

出力:

_thread1
func3
func3 hey hey
thread1 hey hey
thread2
_

_thread t2_、_func2_のデッドロック。見る? _func3_は終了して終了します。再ロックはスレッドをブロックしたり、デッドロックを引き起こしたりしません。


それで、最後の質問、なぜそれが必要なのですか?

再帰関数(マルチスレッドプログラムで呼び出され、リソース/データを保護する場合)。

例えば。マルチスレッドプログラムがあり、スレッドAで再帰関数を呼び出します。その再帰関数で保護するデータがあるため、ミューテックスメカニズムを使用します。その関数の実行はスレッドAでシーケンシャルであるため、再帰で相互排他ロックを確実に再ロックします。通常のミューテックスを使用すると、デッドロックが発生します。そして、これを解決するためにresursive mutexが発明されました。

受け入れられた答えの例を参照してください いつ再帰mutexを使用するのか?

ウィキペディアは、再帰的ミューテックスを非常によく説明しています。間違いなく読む価値があります。 Wikipedia:Reentrant_mutex

1
Rick