mutex の使用

mutex は、相互排他ロックです。 このロックを保持できるのは、1 つのスレッドだけです。

mutex は、データまたはその他のリソースを同時アクセスから保護するために使用されます。 mutex には、mutex の特性を指定する属性があります。

mutex 属性オブジェクト

スレッドと同様、mutex は属性オブジェクトを用いて作成されます。 mutex 属性オブジェクトは抽象的なオブジェクトであり、POSIX オプションのインプリメントの仕方に応じて、 複数の属性があります。 これは、型 pthread_mutexattr_t の変数を使用してアクセスされます。 AIX® では、pthread_mutexattr_tデータ型はポインタである。

mutex 属性オブジェクトの作成と破棄

mutex 属性オブジェクトは、 pthread_mutexattr_init サブルーチンによってデフォルト値に初期化されます。 属性は、サブルーチンによって操作されます。 スレッド属性オブジェクトは、 pthread_mutexattr_destroy サブルーチンによって破棄されます。 このサブルーチンは、スレッド・ライブラリーのインプリメンテーションに応じて、pthread_mutexattr_init サブルーチンによって動的に割り当てられたストレージを解放することができます。

次の例では、mutex 属性オブジェクトを作成し、デフォルト値で初期化し、 その後で使用して最後に破棄します。
pthread_mutexattr_t attributes;
                /* the attributes object is created */
...
if (!pthread_mutexattr_init(&attributes)) {
                /* the attributes object is initialized */
        ...
                /* using the attributes object */
        ...
        pthread_mutexattr_destroy(&attributes);
                /* the attributes object is destroyed */
}

同じ属性オブジェクトを使用して、複数の mutex を作ることができます。 mutex 作成の間で、変更することもできます。 mutex が作成されると、 属性オブジェクトを、作成された mutex に影響を与えずに破棄することができます。

mutex 属性

以下の mutex 属性が定義されています。

属性 説明
プロトコル mutex の優先順位の逆転を避けるために使用されるプロトコルを指定します。 この属性は、優先順位継承または優先順位保護の POSIX オプション のどちらかに依存します。
Process-shared mutex のプロセス共有を指定します。 この属性は、プロセス共有の POSIX オプションに依存します。

これらの属性について詳しくは、「 スレッド・ライブラリー・オプション 」および「 同期スケジューリング」を参照してください。

mutex の作成と破棄

mutex は、 pthread_mutex_init サブルーチンを呼び出すことによって作成されます。 ユーザーは mutex 属性オブジェクトを指定することができます。 NULL ポインターを指定した場合、mutex はデフォルト属性を持ちます。 次のようなコード・フラグメントがあるとします。
pthread_mutex_t mutex;
pthread_mutexattr_t attr;
...
pthread_mutexattr_init(&attr);
pthread_mutex_init(&mutex, &attr);
pthread_mutexattr_destroy(&attr);
これは、以下のものと同等です。
pthread_mutex_t mutex;
...
pthread_mutex_init(&mutex, NULL);

作成された mutex の ID は、 mutex パラメーターを使用してコール側のスレッドに戻されます。 mutex IDは不透明なオブジェクトです。その型は pthread_mutex_tです。 AIXでは、 pthread_mutex_t データ型は構造体です。他のシステムでは、ポインターまたは別のデータ型の場合があります。

mutex は 1 回だけ作成されます。 しかし、pthread_mutex_init サブルーチンを同じ mutex パラメーターで複数回呼び出す (例えば、並行して同じコードを実行している 2 つのスレッドで) ことは避けてください。 mutex 作成の際に固有性を確保する方法には、以下があります。

  • この mutex を使用する他のスレッドを作成する前に、 例えば、初期スレッドで pthread_mutex_init サブルーチンを呼び出す。
  • ワンタイム初期化ルーチン内で、pthread_mutex_init サブルーチンを呼び出す。 詳しくは、 ワンタイム初期化 を参照してください。
  • PTHREAD_MUTEX_INITIALIZER 静的初期化マクロによって初期化された静的 mutex を使用する。mutex はデフォルト属性を持ちます。
mutex が不要になった後で、 pthread_mutex_destroy サブルーチンを呼び出して破棄してください。 このサブルーチンを使用して、 pthread_mutex_init サブルーチンによって割り当てられたストレージを再利用することができます。 mutex を破棄した後、同じ pthread_mutex_t 変数を別の mutex の作成に再使用することができます。 例えば、あまり実用的ではありませんが、次のコード・フラグメントが有効です。
pthread_mutex_t mutex;
...
for (i = 0; i < 10; i++) {
 
        /* creates a mutex */
        pthread_mutex_init(&mutex, NULL);
 
        /* uses the mutex */
 
        /* destroys the mutex */
        pthread_mutex_destroy(&mutex);
}

スレッド間で共有できる他のシステム・リソースと同様に、 スレッドのスタックに割り当てられた mutex は、スレッドが終了する前に破棄する必要があります。 スレッド・ライブラリーは、 mutexのリンク・リストを保守します。 したがって、 mutex が割り振られているスタックが解放されると、リストは破壊されます。

mutex のタイプ

mutex のタイプにより、 操作時に mutex がどのように振る舞うかが決定されます。 次のタイプの mutex があります。
PTHREAD_MUTEX_DEFAULT または PTHREAD_MUTEX_NORMAL
この場合、 最初にアンロックを行わずに 同じ pthread が pthread_mutex_lock サブルーチンを使用して 2 回目のロックを行おうとすると、デッドロックが発生します。 これがデフォルトのタイプです。
PTHREAD_MUTEX_ERRORCHECK
最初に mutex のアンロックを行わずに同じ thread が同じ mutex のロックを複数回試行した場合に、 ゼロ以外の値が戻されることにより、 デッドロックが回避されます。
PTHREAD_MUTEX_RECURSIVE
pthread_mutex_lock サブルーチンを使用して、 同じ pthread が mutex のロックを再帰的に行えるため、 デッドロックが発生したり、 pthread_mutex_lock からゼロ以外の値が戻されることはありません。 この同じ pthread は、pthread_mutex_lock サブルーチンに行ったのと同じ回数、 pthread_mutex_unlock サブルーチンを呼び出して、 他の pthread が使用できるように mutex をアンロックしなければなりません。

mutex 属性は、最初の作成時に、 デフォルト・タイプである PTHREAD_MUTEX_NORMAL になっています。 mutex の作成後、pthread_mutexattr_settype API ライブラリー呼び出しを使用して、タイプを変更できます。

以下に、 再帰的な mutex タイプの作成および使用の例を示します。
pthread_mutexattr_t    attr;
pthread_mutex_t         mutex;

pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex, &attr);

struct {
        int a;
        int b;
        int c;
} A;

f()
{
        pthread_mutex_lock(&mutex);
        A.a++;
        g();
        A.c = 0;
        pthread_mutex_unlock(&mutex);
}

g()
{
        pthread_mutex_lock(&mutex);
        A.b += A.a;
        pthread_mutex_unlock(&mutex);
}

mutex のロックとアンロック

mutex は、ロックとアンロックの 2 つの状態を持つ簡単なロックです。 作成時、mutex はアンロック状態です。 pthread_mutex_lock サブルーチンは、以下の条件下で、指定された mutex をロックします。

  • mutex がアンロック状態の場合、このサブルーチンはそれをロックします。
  • mutex が既に他のスレッドによってロックされている場合、 このサブルーチンは、mutex がアンロックされるまでコール側のスレッドをブロックします。
  • mutex が既にコール側のスレッドによってロックされている場合、サブルーチンは、mutex のタイプによっては、永久にブロックされるか、またはエラーが戻されることがあります。

pthread_mutex_trylock サブルーチンは、以下の条件下では呼び出しスレッドをブロックせずに、 pthread_mutex_lock サブルーチンと同様に動作します。

  • mutex がアンロック状態の場合、このサブルーチンはそれをロックします。
  • mutex が既に任意のスレッドによってロックされている場合、 このサブルーチンはエラーを戻します。

mutex をロックしたスレッドは、 しばしば mutex のオーナー と呼ばれます。

pthread_mutex_unlock サブルーチンは、以下の条件下で呼び出し側の mutex が所有している場合、指定された mutex をアンロック状態にリセットします。

  • mutex が既にアンロックされている場合、このサブルーチンはエラーを戻します。
  • mutex がコール側のスレッドによって所有されていた場合、 このサブルーチンは mutex をアンロックします。
  • mutex が他のスレッドによって所有されていた場合、サブルーチンは、mutex のタイプによっては、 エラーが戻されるか、または mutex がアンロックされることがあります。 mutex は通常、 同じ pthread によってロックとアンロックが行われるため、 このような mutex のアンロックは望ましくありません。

ロックには取り消しポイントがないので、mutex を待っていた間にブロックされたスレッドは取り消すことができません。 したがって、mutex を使用するのは、並行アクセスからのデータの保護と同様に、短時間にとどめることをお勧めします。 詳しくは、「 取り消しポイント 」および「 スレッドの取り消し」を参照してください。

mutex によるデータの保護

mutex は、他のスレッド同期関数を作成するための低レベルのプリミティブとして、 またはデータ保護ロックとして働くように作られています。 ロング・ロックおよびライター優先リーダー/ライター・ロックの実装について詳しくは、 mutex の使用を参照してください。

mutex 使用例

mutex は、データを並行アクセスから保護するために使用することができます。 例えば、 データベース・アプリケーションが、複数の要求を同時に扱うための複数のスレッドを作成する場合があります。 データベース自体は、db_mutex と呼ばれる mutex によって保護されています。 次に例を示します。
/* the initial thread */
pthread_mutex_t mutex;
int i;
...
pthread_mutex_init(&mutex, NULL);    /* creates the mutex      */
for (i = 0; i < num_req; i++)        /* loop to create threads */
        pthread_create(th + i, NULL, rtn, &mutex);
...                                  /* waits end of session   */
pthread_mutex_destroy(&mutex);       /* destroys the mutex     */
...

/* the request handling thread */
...                                  /* waits for a request  */
pthread_mutex_lock(&db_mutex);       /* locks the database   */
...                                  /* handles the request  */
pthread_mutex_unlock(&db_mutex);     /* unlocks the database */
...

初期スレッドは、mutex とすべての要求処理スレッドを作成します。 mutex は、スレッドのエントリー・ポイント・ルーチンのパラメーターを使用して、 スレッドに渡されます。 実際のプログラムでは、mutex のアドレスは、 作成されたスレッドに渡された、さらに複雑なデータ構造体のフィールドである場合があります。

デッドロックの回避

マルチスレッド・アプリケーションでデッドロックが発生する可能性のある状況はいくつかあります。 以下にその一部を示します。
  • デフォルト・タイプである PTHREAD_MUTEX_NORMAL で作成された mutex は、 同じ pthread によって再ロックすると必ずデッドロックが発生します。
  • mutex を逆の順序でロックした場合、 アプリケーションでデッドロックが発生する可能性があります。 例えば、次のコード・フラグメントは、 スレッド A とスレッド B の間にデッドロックを発生させます。
    /* Thread A */
    pthread_mutex_lock(&mutex1);
    pthread_mutex_lock(&mutex2);
    
    /* Thread B */
    pthread_mutex_lock(&mutex2);
    pthread_mutex_lock(&mutex1);
  • アプリケーションでは、 リソース・デッドロックというデッドロックが発生する場合があります。 次に例を示します。
    struct {
                    pthread_mutex_t mutex;
                    char *buf;
            } A;
    
    struct {
                    pthread_mutex_t mutex;
                    char *buf;
            } B;
    
    struct {
                    pthread_mutex_t mutex;
                    char *buf;
            } C;
    
    use_all_buffers()
    {
            pthread_mutex_lock(&A.mutex);
            /* use buffer A */
    
            pthread_mutex_lock(&B.mutex);
            /* use buffers B */
    
            pthread_mutex_lock(&C.mutex);
            /* use buffer C */
    
            /* All done */
            pthread_mutex_unlock(&C.mutex);
            pthread_mutex_unlock(&B.mutex);
            pthread_mutex_unlock(&A.mutex);
    }
    
    use_buffer_a()
    {
            pthread_mutex_lock(&A.mutex);
            /* use buffer A */
            pthread_mutex_unlock(&A.mutex);
    }
    
    functionB()
    {
            pthread_mutex_lock(&B.mutex);
            /* use buffer B */
            if (..some condition)
            { 
              use_buffer_a();
            }
            pthread_mutex_unlock(&B.mutex);
    }
    
    /* Thread A */
    use_all_buffers();
    
    /* Thread B */
    functionB();
    このアプリケーションには、 thread Athread Bの 2 つのスレッドがあります。 Thread B は最初に実行を開始し、その後すぐに thread A が開始します。 thread Ause_all_buffers () を実行し、 A.mutexを正常にロックした場合、 thread B が既にロックしているため、 B.mutexをロックしようとするとブロックされます。 thread BfunctionB を実行している間、 thread A がブロックされている間に some_condition が発生すると、 thread B は、 thread Aによって既にロックされている A.mutexの獲得の試行もブロックするようになります。 この結果、デッドロックが発生します。

    このデッドロックを解決するには、 リソースを使用する前に、 必要とするすべてのリソース・ロックを各スレッドで獲得してください。 ロックを獲得できなければ、それらを解除して再度開始する必要があります。

mutex と競争状態

相互排他ロック (mutex) を使用することにより、 競争状態によるデータの不整合を防ぐことができます。 競争状態は、2 つ以上のスレッドが同じメモリー領域の操作を実行する必要があるときにしばしば発生しますが、 計算の結果は、それらの操作の実行順序によって決まります。

例えば、 1 つのカウンター Xが、 A と B の 2 つのスレッドによって増分されるとします。 X が元々 1の場合、スレッド A と B がカウンターを増分するときには、 X は 3でなければなりません。 どちらのスレッドも独立エンティティーであり、相互に同期化されません。 ただし、C ステートメントは、X++以下の疑似アセンブラー・コードに示すように、アトミックであるにもかかわらず、生成されたアセンブリー・コードがそうでない可能性があります。
move    X, REG
inc     REG
move    REG, X

上記の例の両方のスレッドが 2 つの CPU で同時に実行される場合、 またはスケジューリングによってスレッドがそれぞれの命令を交互に実行する場合は、 次のようなステップになる可能性があります。

  1. スレッド A が最初の命令を実行して、X (これは 1) をスレッド A のレジスターに書き込みます。 その後、スレッド B が実行して、X (これは 1) をスレッド B のレジスターに書き込みます。 次の例は、実行後のレジスターとメモリー X の内容を示しています。
    Thread A Register = 1
    Thread B Register = 1
    Memory X          = 1
  2. スレッド A は 2 番目の命令を実行し、そのレジスターの内容を 2に増分します。 その後、スレッド B はそのレジスターを 2に増分します。 メモリー Xには何も移動されません。したがって、メモリーは X ままになります。 次の例は、実行後のレジスターとメモリー X の内容を示しています。
    Thread A Register = 2
    Thread B Register = 2
    Memory X          = 1
  3. スレッド A は、レジスターの内容をメモリー Xに移動します。この内容は、現在 2になります。 次に、スレッド B はそのレジスターの内容を ( 2でもある ) メモリー Xに移動し、スレッド Aの値を上書きします。 次の例は、実行後のレジスターとメモリー X の内容を示しています。
    Thread A Register = 2
    Thread B Register = 2
    Memory X          = 2

多くの場合、スレッド A とスレッド B は 3 つの命令を順次に実行し、 結果は予想どおりに 3 になります。 競争状態は断続的に起こるので、 通常は検出することが困難です。

この競合状態を回避するには、カウンターにアクセスしてメモリー Xを更新する前に各スレッドがデータをロックする必要があります 例えば、スレッド A がロックを取り、カウンターを更新した場合、スレッドはメモリー X を 2 の値で残します。 スレッド A がロックを解放すると、スレッド B はロックを取り、カウンターを更新し、 2 を X の初期値として取り、それを 3に増分します。