スレッド同期: ミューテックス、条件変数、スピン ロック、読み取り/書き込みロック

画像

そして彭世才をもっと近くで見つけたいと思い、菊に酔いしれる。


シングルスレッドのプロセスの場合、スレッドの同期 に対処する必要がないため 、マルチスレッド環境ではスレッドの同期に注意が必要になる可能性があります。スレッドの主な利点は、グローバル変数を介した情報共有などのリソースの共有にありますが、この便利な共有には代償が伴います。つまり、複数のスレッドが共有データに同時にアクセスすることによって引き起こされるデータの不整合の問題 です

以下のトピックについて話し合います。

スレッド同期が必要な理由、
スレッド同期ミューテックス、
スレッド同期セマフォ、
スレッド同期条件変数、
スレッド同期読み取り/書き込みロック。

1 なぜスレッド同期が必要なのでしょうか?

スレッド同期は、共有リソースへのアクセスを保護するために行われますここで言う共有リソースとは、複数のスレッドがアクセスするリソースのことで、例えば、グローバル変数aが定義されている場合、スレッド1が変数aにアクセスし、スレッド2も変数aにアクセスすると、この時点で変数aは複数になります。スレッド間で共有リソースがある場合、全員がそれにアクセスする必要があります。

保護の目的は、データの一貫性の問題を解決することですもちろん、データの整合性の問題はどのような状況で発生するのか、状況に応じて区別されます。各スレッドによってアクセスされる変数は、他のスレッドによって読み取られ、変更されることはありません。(スレッド関数で定義されたローカル変数、または 1 つのスレッドのみがアクセスするグローバル変数など)、データの一貫性の問題はありません。变量是只读的,多个线程同时读取该变量也不会有数据一致性的问题;但是,当一个线程可以修改的变量,其它的线程也可以读取或者修改的时候,这个时候就存在数据一致性的问题,需要对这些线程进行同步操作,确保它们在访问变量的存储内容时不会访问到无效的值。

出现数据一致性问题其本质在于进程中的多个线程对共享资源的并发访问(同时访问)。进程中的多个线程间是并发执行的,每个线程都是系统调用的基本单元,参与到系统调度队列中;对于多个线程间的共享资源,并发执行会导致对共享资源的并发访问,并发访问所带来的问题就是竞争(如果多个线程同时对共享资源进行访问就表示存在竞争,跟现实生活当中的竞争有一定的相似之处,譬如一个队伍当中需要选出一名队长,现在有两个人在候选名单中,那么意味着这两个人就存在竞争关系),并发访问就可能会出现数据一致性问题,所以就需要解决这个问题;要防止并发访问共享资源,那么就需要对共享资源的访问进行保护,防止出现并发访问共享资源。

当一个线程修改变量时,其它的线程在读取这个变量时可能会看到不一致的值,图 1.1 描述了两个线程读写相同变量(共享变量、共享资源)的假设例子。在这个例子当中,线程 A 读取变量的值,然后再给这个变量赋予一个新的值,但写操作需要 2 个时钟周期(这里只是假设);当线程 B 在这两个写周期中间读取了这个变量,它就会得到不一致的值,这就出现了数据不一致的问题。

ここに画像の説明を挿入

我们可以编写一个简单地代码对此文件进行测试,示例代码 1.1 展示了在 2 个线程在常规方式下访问共享资源,这里的共享资源指的就是静态全局变量 g_count。该程序创建了两个线程,且均执行同一个函数,该函数执行一个循环,重复以下步骤:将全局变量 g_count 复制到本地变量 l_count 变量中,然后递增 l_count,再把 l_count 复制回 g_count,以此不断增加全局变量 g_count 的值。因为 l_count 是分配于线程栈中的自动变量(函数内定义的局部变量),所以每个线程都有一份。循环重复的次数要么由命令行参数指定,要么去默认值 1000 万次,循环结束之后线程终止,主线程回收两个线程之后,再将全局变量 g_count 的值打印出来。

//示例代码 1.1
 #include <stdio.h>
 #include <stdlib.h>
 #include <pthread.h>
 #include <unistd.h>
 #include <string.h>
  
 static int loops;
 static int g_count = 0;
 
 static void *new_thread_start(void *arg)
 {
    
    
     int loops = *((int *)arg);
     int l_count, j;
     for (j = 0; j < loops; j++) {
    
    
         l_count = g_count;
         l_count++;
         g_count = l_count;
     }
     return (void *)0;
 }

 int main(int argc, char *argv[])
 {
    
    
     pthread_t tid1, tid2;
     int ret;
     /* 获取用户传递的参数 */
     if (2 > argc)
         loops = 10000000; //没有传递参数默认为 1000 万次
     else
         loops = atoi(argv[1]);

     /* 创建 2 个新线程 */
     ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
     if (ret) {
    
    
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
         exit(-1);
     }

     ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
     if (ret) {
    
    
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
         exit(-1);
     }

     /* 等待线程结束 */
     ret = pthread_join(tid1, NULL);
     if (ret) {
    
    
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
         exit(-1);
     }

     ret = pthread_join(tid2, NULL);
     if (ret) {
    
    
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
         exit(-1);
     }

     /* 打印结果 */
     printf("g_count = %d\n", g_count);
     exit(0);
 }

编译代码,进行测试,首先执行代码,传入参数1000,也就是让每个线程对全局变量 g_count 递增 1000 次,如下所示:

sundp:~/codetest$ gcc test.c -lpthread
sundp:~/codetest$ ./a.out 1000
g_count = 2000

都打印结果看,得到了我们想象中的结果,每个线程递增 1000 次,最后的数值就是 2000;接着我们把递增次数加大,采用默认值 1000 万次,如下所示

sundp:~/codetest$ gcc test.c -lpthread
sundp:~/codetest$ ./a.out
g_count = 10459972

可以发现,结果竟然不是我们想看到的样子,执行到最后,应该是 2000 万才对,这里其实就出现图 1.1 中所示的问题,数据不一致。

如何解决对共享资源的并发访问出现数据不一致的问题?

図 1.1 のデータの不整合の問題を解決するには、Linux が提供するいくつかのメソッド、つまりスレッド同期技術を使用して、同時に 1 つのスレッドのみが変数にアクセスできるようにし、同時アクセスを防ぎ、データの不整合を解消する必要があります。 。

図 1.2 は、この同期操作を示しています。図から、スレッド A もスレッド B も同時にこの変数にアクセスしないことがわかります。スレッド A が変数の値を変更する必要がある場合、書き込み操作が完了するまで待機する必要があります。完了 (中断できません))、スレッド B を実行して読み取りを行う前に。

スレッドの主な利点は、グローバル変数を介した情報共有など、リソースの共有にあります。ただし、この便利な共有には代償が伴い、複数のスレッドが同じ変数を同時に変更しないこと、またはスレッドが他のスレッドによって変更されている変数を読み取らないことを保証する必要があります。Linux システムは、スレッド同期のためのさまざまなメカニズムを提供します。一般的な方法には、ミューテックス ロック、条件変数、スピン ロック、読み取り/書き込みロックなどが含まれます。

2 つのミューテックス

ミューテックス (ミューテックス) はミューテックスとも呼ばれますが、これは本質的にはロックです。ミューテックスは共有リソースにアクセスする前にロックされ、アクセスが完了するとミューテックスは解放 (ロック解除) されます。ミューテックスはロック後に、他のスレッドが試行します。ミューテックスを再度ロックすることは、現在のスレッドがミューテックスを解放するまでブロックされます。ミューテックスの解放時に複数のスレッドがブロックされている場合、これらのブロックされたスレッドが起動され、すべてのスレッドがミューテックスをロックしようとします。1 つのスレッドがミューテックスのロックに成功すると、他のスレッドはロックできなくなり、再びロックされます。再度ブロックするには、次のロック解除を待つ必要があります。

非常にシンプルでわかりやすい例として、トイレ(共有リソース)を例に挙げます。人(スレッド)が来て、トイレに誰もいないのを確認すると、入ってドアをロックします。内側から (ミューテックスのロック) ); この時点でさらに 2 人 (スレッド) が来て、便宜上バスルームに入ろうとしましたが、この時点ではドアを開けることができませんでした (ミューテックスのロックに失敗しました)。中に人がいたのでこの時は待つしかなかった(トラップブロッキング); 中の人の都合が終わったら(共有リソースへのアクセスが完了)、ロックを開けて(ミューテックスロック解除)、中から出てくる現時点では、外で待っている人が 2 人います。ロックを拒否してロックします)、当然 2 人は 1 つの部屋にしか入れず、入った人は再びドアをロックし、もう 1 人はドアがロックされるのを待ち続けることしかできません。出てくる。

私たちのプログラミングでは、共有リソースにアクセスするすべてのスレッドが同じデータ アクセス ルールで設計されている場合にのみ、ミューテックスは正常に動作します。いずれかのスレッドがロックを取得せずに共有リソースにアクセスできる場合、共有リソースを使用する前に他のスレッドがロックを申請しても、データの不整合が発生します。

ミューテックスはpthread_mutex_tデータ型を使用して、ミューテックスを使用する前に最初に初期化する必要があることを示します。ミューテックスは 2 つの方法で初期化できます。

2.1 ミューテックスの初期化

1. PTHREAD_MUTEX_INITIALIZER マクロを使用してミューテックスを初期化します。

以下に示すように、ミューテックスはpthread_mutex_tデータ型 (pthread_mutex_t実際には構造体型) で表され、マクロはPTHREAD_MUTEX_INITIALIZER実際には構造体代入操作のパッケージです。

#define PTHREAD_MUTEX_INITIALIZER
{ { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }

PTHREAD_MUTEX_INITIALIZERしたがって、マクロを使用してミューテックスを初期化する操作は次のようになります。

pthread_mutex_t ミューテックス = PTHREAD_MUTEX_INITIALIZER;

PTHREAD_MUTEX_INITIALIZER マクロは、ミューテックスのデフォルト属性をすでに持っています。

2. pthread_mutex_init() 関数を使用してミューテックスを初期化します。

マクロの使用はPTHREAD_MUTEX_INITIALIZER、定義時に直接初期化する場合にのみ適しています。この方法は、最初にミューテックスを定義してから初期化する場合や、malloc ( ) 関数を使用して、割り当てられたミューテックス オブジェクトに適用すると、このような場合、 pthread_mutex_init() 関数を使用してミューテックスを初期化できます。その
関数のプロトタイプは次のとおりです。

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

この機能を使用するには、ヘッダー ファイルをインクルードする必要があります<pthread.h>

関数のパラメータと戻り値は次の意味を持ちます。

mutex: パラメータ mutex はpthread_mutex_t、初期化する必要があるミューテックス オブジェクトを指す型ポインタです;
attr: パラメータ attr は、型オブジェクトpthread_mutexattr_tを指すpthread_mutexattr_t型ポインタであり、ミューテックスの属性を定義するために使用されます。パラメータattrが設定されている NULLの場合、ミューテックスの属性がデフォルト値に設定されていることを意味します この場合、PTHREAD_MUTEX_INITIALIZERこのように初期化するのと同等ですが、マクロを使用するとエラーにならない点が異なりますチェック中。
戻り値: 成功した場合は 0 を返し、失敗した場合は 0 以外のエラー コードを返します。

ヒント: "man 3 pthread_mutex_init"Ubuntu システムでコマンドを実行すると、関数が見つからないというメッセージが表示されますが、Linux ではその関数が存在しないのではなく、その関数に関連するマニュアルのヘルプ情報がインストールされていません。 . 現時点では、インストールを実行するだけです"sudo apt-get install manpages-posix-dev"

関数を使用してpthread_mutex_init()ミューテックスを初期化する例:

pthread_mutex_t ミューテックス;
pthread_mutex_init(&mutex, NULL);

または:

pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(ミューテックス, NULL);

2.2 ミューテックスのロックとロック解除

初期化後のミューテックスはアンロック状態となり、呼び出し側関数はpthread_mutex_lock()ミューテックスをロックして取得し、呼び出し側関数はpthread_mutex_unlock()ロックを解除してミューテックスを解放することができます。その関数プロトタイプは次のとおりです。

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

これらの関数を使用するには、ヘッダー ファイル<pthread.h>、パラメータ mutex が mutex オブジェクトを指す必要があり、呼び出しが成功すると 0 を返し、失敗すると 0 以外のエラー コードを返しますpthread_mutex_lock()pthread_mutex_unlock()

関数を呼び出してpthread_mutex_lock()ミューテックスをロックします。ミューテックスがロック解除されている場合、呼び出しは正常にロックされ、関数呼び出しはすぐに戻ります。この時点でミューテックスが他のスレッドによってロックされている場合、呼び出しはミューテックスがロックされるまでブロックされますpthread_mutex_lock()。ロックが解除されている場合、その時点で呼び出しはミューテックスをロックして戻ります。

関数を呼び出してpthread_mutex_unlock()、すでにロックされているミューテックスをロック解除します。次のようなことは間違っています。

ロック解除状態にあるミューテックスのロックを解除します。
別のスレッドによってロックされているミューテックスのロックを解除します。

ミューテックスのロック解除を待機しているブロック状態のスレッドが複数ある場合、pthread_mutex_unlock()現在ミューテックスをロックしている関数を呼び出すスレッドによってミューテックスのロックが解除されると、待機中のこれらのスレッドにはミューテックスをロックする機会がありますが、ミューテックスをロックすることは不可能です。どちらかを決定してください。スレッドは望むことを実行します。

使用例

サンプルコード 1.1 をミューテックスを使用して変更する 変更後は、サンプルコードに示すように、ミューテックスを使用してグローバル変数 g_count へのアクセスが保護されます。

//示例代码 2.1 
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static int loops;
static int g_count = 0;
static pthread_mutex_t mutex;

static void *new_thread_start(void *arg)
{
    
    
    int loops = *((int *)arg);
    int l_count, j;
    for (j = 0; j < loops; j++) {
    
    
        pthread_mutex_lock(&mutex); //互斥锁上锁
 
        l_count = g_count;
        l_count++;
        g_count = l_count;
        
        pthread_mutex_unlock(&mutex);//互斥锁解锁
    }
    return (void *)0;
}

int main(int argc, char *argv[])
{
    
    
    pthread_t tid1, tid2;
    int ret;

    /* 获取用户传递的参数 */
    if (2 > argc)
        loops = 10000000; //没有传递参数默认为 1000 万次
    else
        loops = atoi(argv[1]);

    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex, NULL);

    /* 创建 2 个新线程 */
    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
    if (ret) {
    
    
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
    if (ret) {
    
    
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 等待线程结束 */
    ret = pthread_join(tid1, NULL);
    if (ret) {
    
    
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_join(tid2, NULL);
    if (ret) {
    
    
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 打印结果 */
    printf("g_count = %d\n", g_count);
    exit(0);
}

デフォルトでは、1000w 回実行され、結果は次のようになります。

sundp:~/codetest$ gcc test.c -lpthread
sundp:~/codetest$ ./a.out
g_count = 20000000

g_count実際に見たい正しい結果が得られており、各時間の累積は常に正しいことがわかりますが、プログラムの実行過程では、ロックに費やされる時間が比較的長くなることがわかります。 、これには後で紹介するパフォーマンスの問題が関係します。

2.3 pthread_mutex_trylock() 関数

ミューテックスが他のスレッドによってロックされている場合、呼び出し元のpthread_mutex_lock()関数は、ミューテックスのロックが解除されるまでブロックされます。スレッドがブロックされたくない場合は、pthread_mutex_trylock()その関数を使用できます。呼び出し元のpthread_mutex_trylock()関数は、ミューテックスがロックされている場合、ミューテックスをロックしようとします。ロックが解除されている場合、通話pthread_mutex_trylock()は行われますミューテックスをロックし、すぐに戻ります、ミューテックスが他のスレッドによってロックされている場合、 pthread_mutex_trylock() の呼び出しはロックに失敗しますが、ブロックはされませんが、エラー コード EBUSY が返されます。

その関数プロトタイプは次のとおりです。

#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);

パラメータ mutex はターゲット ミューテックスを指し、成功すると 0 を返し、失敗すると 0 以外のエラー コードを返し、ターゲット ミューテックスが他のスレッドによってロックされている場合は EBUSY を返します。

使用例
サンプルコード 2.1 を、 をpthread_mutex_trylock()置き換えて修正しますpthread_mutex_lock()

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static int loops;
static int g_count = 0;
static pthread_mutex_t mutex;

static void *new_thread_start(void *arg)
{
    
    
    int loops = *((int *)arg);
    int l_count, j;
    for (j = 0; j < loops; j++) {
    
    
        while(pthread_mutex_trylock(&mutex));
 
        l_count = g_count;
        l_count++;
        g_count = l_count;
        //printf("tid is %ld g_count is %d \n",pthread_self(),g_count);
        pthread_mutex_unlock(&mutex);//互斥锁解锁
    }
    return (void *)0;
}

int main(int argc, char *argv[])
{
    
    
    pthread_t tid1, tid2;
    int ret;

    /* 获取用户传递的参数 */
    if (2 > argc)
        loops = 10000000; //没有传递参数默认为 1000 万次
    else
        loops = atoi(argv[1]);

    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex, NULL);

    /* 创建 2 个新线程 */
    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
    if (ret) {
    
    
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
    if (ret) {
    
    
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 等待线程结束 */
    ret = pthread_join(tid1, NULL);
    if (ret) {
    
    
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_join(tid2, NULL);
    if (ret) {
    
    
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 打印结果 */
    printf("g_count = %d\n", g_count);
    exit(0);
}

実行結果全体はpthread_mutex_lock()使用効果と同じであり、自分でテストできます。

2.4 ミューテックスを破棄する

ミューテックスが不要になった場合は、pthread_mutex_destroy()ミューテックスを破棄する関数を呼び出して破棄する必要があります。その関数のプロトタイプは次のとおりです。

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);

この関数を使用するには、ヘッダー ファイルをインクルードする必要があり<pthread.h>、パラメータ mutex はターゲットの mutex を指します。また、呼び出しが成功した場合は 0 を返し、失敗した場合は 0 以外のエラー コードを返します。

ロックが解除されていないミューテックスは破棄できず、破棄しないとエラーが発生します。また、
初期化されていないミューテックスも破棄できません。

ミューテックスがpthread_mutex_destroy()破棄されると、ロックやロック解除ができなくなり、pthread_mutex_init()ミューテックスを使用する前に再度呼び出してミューテックスを初期化する必要があります。

使用例

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static int loops;
static int g_count = 0;
static pthread_mutex_t mutex;

static void *new_thread_start(void *arg)
{
    
    
    int loops = *((int *)arg);
    int l_count, j;
    for (j = 0; j < loops; j++) {
    
    
        while(pthread_mutex_trylock(&mutex));
 
        l_count = g_count;
        l_count++;
        g_count = l_count;
        //printf("tid is %ld g_count is %d \n",pthread_self(),g_count);
        pthread_mutex_unlock(&mutex);//互斥锁解锁
    }
    return (void *)0;
}

int main(int argc, char *argv[])
{
    
    
    pthread_t tid1, tid2;
    int ret;

    /* 获取用户传递的参数 */
    if (2 > argc)
        loops = 10000000; //没有传递参数默认为 1000 万次
    else
        loops = atoi(argv[1]);

    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex, NULL);

    /* 创建 2 个新线程 */
    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
    if (ret) {
    
    
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
    if (ret) {
    
    
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 等待线程结束 */
    ret = pthread_join(tid1, NULL);
    if (ret) {
    
    
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_join(tid2, NULL);
    if (ret) {
    
    
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 打印结果 */
    printf("g_count = %d\n", g_count);
    exit(0);

    /* 销毁互斥锁 */
    pthread_mutex_destroy(&mutex);
    exit(0);
}

2.5 ミューテックスのデッドロック

ただ想像します、スレッドが同じミューテックスを 2 回ロックしようとした場合、 何が起こるのですか?状況はスレッドはデッドロック状態に陥り、永久にブロックされます。; これはデッドロックが発生する状況です。さらに、デッドロックを引き起こす可能性のあるミューテックスの使用方法は他にもたくさんあります。

場合によっては、スレッドが 2 つ以上の異なる共有リソースに同時にアクセスする必要があり、各リソースは異なるミューテックスによって管理されます。デッドロックは、複数のスレッドが同じミューテックスのセット (2 つ以上のミューテックス) をロックすると発生する可能性があります。; たとえば、プログラムで複数のミューテックスが使用されている場合、スレッドが最初のミューテックスを常に保持することが許可されており、2 番目のミューテックスをロックしようとするとブロックされるが、2 番目のミューテックスを所有している場合、スレッドはまたロックしようとします。最初のミューテックスをロックします。両方のスレッドがもう一方のスレッドが所有するリソースを要求しているため、どちらのスレッドも先に進むことができず、永久にブロックされるため、デッドロックが発生します。以下のサンプルコードに示すように:

// 回線程 A
pthread_mutex_lock(mutex1);
pthread_mutex_lock(mutex2);
// 線程 B
pthread_mutex_lock(mutex2);
pthread_mutex_lock(mutex1);

これは、C 言語における 2 つのヘッダー ファイル間の相互包含関係に似ており、コンパイル時に必ずエラーが報告されます。

私たちのプログラムでは、複数のミューテックスが使用されている場合、そのようなデッドロックを回避する最も簡単な方法は、ミューテックスの階層関係を定義することです。複数のスレッドがミューテックスのセットで動作するとき、ミューテックスのセットは常に同じ順序でロックされる必要があります。たとえば、上記のシナリオでは、2 つのスレッドが常に最初に mutex1 をロックし、次に mutex2 をロックする場合、デッドロックは発生しません。ミューテックス間の階層関係のロジックが十分に明確でない場合もありますが、それでも、すべてのスレッドが従わなければならない必須の階層順序を設計することは可能です。

しかし、アプリケーション プログラムの構造により、ミューテックスのソートが困難になる場合があります。プログラムは複雑で、多くのミューテックスと共有リソースが関係しています。プログラムの設計上、ミューテックスのグループを同じ順序でソートすることは実際には不可能です。ロックがロックされている場合は、別の方法を使用する必要があります。

たとえば、pthread_mutex_trylock()非ブロック的な方法でミューテックスをロックしようとすると、スレッドはまず関数を使用してpthread_mutex_lock()最初のミューテックスをロックし、次にその関数を使用してpthread_mutex_trylock()残りのミューテックスをロックします。いずれかの呼び出しが失敗した場合pthread_mutex_trylock()(EBUSY が返された場合)、スレッドはすべてのミューテックスを解放し、しばらく経ってから最初から再試行できます。階層関係に基づいてデッドロックを回避する最初の方法と比較すると、この方法は複数のループを経由する必要があるため効率が低くなります。

ミューテックスデッドロックの問題を解決する方法はまだたくさんあり、筆者も詳しく勉強したわけではありませんが、実際のプログラミングアプリケーションでこの知識を使用する必要がある場合は、関連する資料や書籍を参照して学習してください。

使用例
長い間考えましたが、これ以上に適切な例はありません。一旦休憩しましょう。

2.6 ミューテックスのプロパティ

前述したように、pthread_mutex_init()ミューテックスの属性は、パラメータ attr で指定されるミューテックスを初期化するために関数が呼び出されるときに設定できます。

パラメータ attr は、 mutex のプロパティを定義するpthread_mutexattr_t型オブジェクトを指します。もちろん、パラメータ attr が NULL に設定されている場合は、mutex プロパティがデフォルト値に設定されていることを意味します。ミューテックスのプロパティについて ミューテックス プロパティの詳細について詳しく説明するつもりはありません。また、型に定義されているプロパティを 1 つずつリストするつもりもありません。デフォルト属性が使用されない場合、関数を呼び出すとき、パラメーター attr はNULL ではなくオブジェクトを指す必要があります。オブジェクトを定義した後、関数を使用してオブジェクトを初期化する必要があります。オブジェクトが使用されなくなった場合は、そのオブジェクトを破棄するために関数を使用する必要があります。関数のプロトタイプは次のとおりです。pthread_mutexattr_t pthread_mutex_init()pthread_mutexattr_tpthread_mutexattr_tpthread_mutexattr_init()pthread_mutexattr_destroy()

#include <pthread.h>
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);

パラメータ attr は、初期化する必要があるオブジェクトを指しますpthread_mutexattr_t。呼び出しが成功した場合は 0 を返し、失敗した場合は 0 以外のエラー コードを返します。

pthread_mutexattr_init()関数はデフォルトを使用しますミューテックス属性初始化参数 attr 指向的 pthread_mutexattr_t 对象。
关于互斥锁的属性比较多,譬如进程共享属性健壮属性类型属性等等,这里并不会一一给大家进行介绍,本小节讨论下类型属性,其它的暂时不去解释了。

互斥锁的类型属性控制着互斥锁的锁定特性,一共有 4 中类型:

PTHREAD_MUTEX_NORMAL:一种标准的互斥锁类型,不做任何的错误检查或死锁检测。如果线程试图对已经由自己锁定的互斥锁再次进行加锁,则发生死锁;互斥锁处于未锁定状态,或者已由其它线程锁定,对其解锁会导致不确定结果。

PTHREAD_MUTEX_ERRORCHECK:此类互斥锁会提供错误检查。譬如这三种情况都会导致返回错误:线程试图对已经由自己锁定的互斥锁再次进行加锁(同一线程对同一互斥锁加锁两次),返回错误;线程对由其它线程锁定的互斥锁进行解锁,返回错误;线程对处于未锁定状态的互斥锁进行解锁,返回错误。这类互斥锁运行起来比较慢,因为它需要做错误检查,不过可将其作为调试工具,以发现程序哪里违反了互斥锁使用的基本原则。
PTHREAD_MUTEX_RECURSIVE:此类互斥锁允许同一线程在互斥锁解锁之前对该互斥锁进行多次加锁,然后维护互斥锁加锁的次数,把这种互斥锁称为递归互斥锁,但是如果解锁次数不等于加速次数,则是不会释放锁的;所以,如果对一个递归互斥锁加锁两次,然后解锁一次,那么这个互斥锁依然处于锁定状态,对它再次进行解锁之前不会释放该锁。
PTHREAD_MUTEX_DEFAULT :此类互斥锁提供默认的行为和特性。使用宏PTHREAD_MUTEX_INITIALIZER 初始化的互斥锁 ,或者调用参数arg 为 NULL 的pthread_mutexattr_init()函数所创建的互斥锁,都属于此类型。此类锁意在为互斥锁的实现保留最大灵活性, Linux 上 ,PTHREAD_MUTEX_DEFAULT 类 型 互 斥 锁 的 行 为 与PTHREAD_MUTEX_NORMAL 类型相仿。

関数を使用してpthread_mutexattr_gettype()ミューテックスの type 属性を取得し、pthread_mutexattr_settype()modify を使用してミューテックスの type 属性を設定できます。関数のプロトタイプは次のとおりです。

#include <pthread.h>
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int 型);

これらの関数を使用するには、ヘッダー ファイルをインクルードする必要があり<pthread.h>、パラメーター attr はpthread_mutexattr_t型オブジェクトを指します。pthread_mutexattr_gettype()関数の場合、関数が正常に呼び出された場合、ミューテックス型属性はパラメーター型が指すメモリに保存されます。関数の場合、pthread_mutexattr_settype()パラメータ attr は、指定されたオブジェクトの type プロパティが、pthread_mutexattr_tパラメータの type で指定された型に設定されます。使用方法は次のとおりです。

pthread_mutex_t mutex;
pthread_mutexattr_t attr;
/* 初始化互斥锁属性对象 */
pthread_mutexattr_init(&attr);
/* 将类型属性设置为 PTHREAD_MUTEX_NORMAL */
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
/* 初始化互斥锁 */
pthread_mutex_init(&mutex, &attr);
......
/* 使用完之后 */
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);

3 条件変数

このセクションでは、スレッド同期の 2 番目の方法である条件変数について説明します。

条件変数は、スレッドで使用できるもう 1 つの同期メカニズムです。条件変数は、特定のイベントが発生するか条件が満たされるまでスレッドを自動的にブロックするために使用されます。通常、条件変数はミューテックスと組み合わせて使用​​されます。条件変数の使用には主に 2 つのアクションが含まれます。

1 つのスレッドは特定の条件が満たされるのを待ってブロックされ、
別のスレッドでは条件が満たされると「シグナル」が送信されます。

この問題を説明するために、条件変数を使用しない例 (生産者/消費者モデル) を見てみましょう。生産者は製品を生産する責任があり、消費者は製品を消費する責任があります。消費者にとって、製品がない場合は、商品が出たら商品があるときに使いましょう。

ここでは、この製品を表すために変数を使用します。生産者は製品変数に 1 を加えたものを生成し、消費者は変数から 1 を引いたものを消費します。サンプル コードは次のとおりです。

//3.1 生产者---消费者示例代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static pthread_mutex_t mutex;
static int g_avail = 0;

/* 消费者线程 */
static void *consumer_thread(void *arg)
{
    for ( ; ; ) {
        pthread_mutex_lock(&mutex);//上锁
        while (g_avail > 0)
        g_avail--; //消费
        pthread_mutex_unlock(&mutex);//解锁
    }
    return (void *)0;
}

/* 主线程(生产者) */
int main(int argc, char *argv[])
{
    pthread_t tid;
    int ret;
    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex, NULL);
    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, consumer_thread, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    for ( ; ; ) {
        pthread_mutex_lock(&mutex);//上锁
        g_avail++; //生产
        pthread_mutex_unlock(&mutex);//解锁
    }
    exit(0);
}

このコードでは、メインスレッドが「プロデューサー」として機能し、新しく作成されたスレッドが「コンシューマー」として機能します。実行後はすべて無限ループになるため、ミューテックスの破棄や待機に関連するコードは追加されません。新しいスレッドがリサイクルされるため、プロセスが終了すると自動的に処理されます。

上記のコードは機能しますが、次の理由により、新しいスレッドはグローバル変数 g_avail が 0 より大きいかどうかを継続的にチェックするため、CPU リソースが無駄に消費されます。この問題は、条件変数を使用することで簡単に解決できます。条件変数を使用すると、スレッドは、別のスレッドから通知される (シグナルを受信する) まで、独自の操作を実行する前にスリープ (ブロックして待機) することができます。たとえば、上記のコードで、条件 g_avail > 0 が true でない場合、コンシューマ スレッドはスリープ状態に入り、プロデューサがプロダクトを生成した後 (g_avail++、この時点で g_avail は 0 より大きくなります)、待機状態のスレッドに「シグナル」を送信し、他のスレッドはその後ウェイクアップします。 「シグナル」を受信中!

前述したように、条件変数は通常、ミューテックス ロックとともに使用されます。条件の検出はミューテックスの保護の下で実行されるため、つまり、条件自体がミューテックスによって保護されており、スレッドは条件の状態を変更する前にまずミューテックスをロックする必要があります。そうしないと、スレッドがロックされてしまう可能性があります。スレッドが失敗する原因になります。安全上の問題です。

3.1 条件変数の初期化

条件変数はpthread_cond_t、ミューテックスと同様のデータ型を使用して表され、使用する前に初期化する必要があります。PTHREAD_COND_INITIALIZER初期化方法もマクロを使用する方法と関数を使用する方法の2 つがありますがpthread_cond_init()、マクロを使用する初期化方法はミューテックスの場合と同じであるため、ここでは繰り返しません。
例えば:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

pthread_cond_init()関数のプロトタイプは次のようになります。

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

また、これらの関数を使用するには、ヘッダー ファイルをインクルード<pthread.h>し、pthread_cond_init()関数を使用して条件変数を初期化し、その関数を使用して条件変数が使用されなくなったときに条件変数を破棄する必要がありますpthread_cond_destroy()

パラメータは条件変数オブジェクトcondを指します。関数の場合、ミューテックスに似ています。条件変数が初期化されると、条件変数のプロパティが設定されます。パラメータは型オブジェクトを指しデータ型が使用されます。条件変数のプロパティを説明します。このパラメーターは NULL に設定でき、マクロを使用するのと同じように、属性のデフォルト値が条件変数の初期化に使用されることを示します。pthread_cond_tpthread_cond_init()attrpthread_condattr_tpthread_condattr_tattrPTHREAD_COND_INITIALIZER

関数呼び出しは正常に 0 を返し、失敗した場合は 0 以外のエラー コードを返します。

初期化および破棄操作では、次の問題に注意する必要があります。

在使用条件变量之前必须对条件变量进行初始化操作,使用 PTHREAD_COND_INITIALIZER 宏或者函数 pthread_cond_init()都行;
对已经初始化的条件变量再次进行初始化,将可能会导致未定义行为;
对没有进行初始化的条件变量进行销毁,也将可能会导致未定义行为;
对某个条件变量而言,仅当没有任何线程等待它时,将其销毁才是最安全的;
经 pthread_cond_destroy()销毁的条件变量,可以再次调用 pthread_cond_init()对其进行重新初始化。

3.2 通知和等待条件变量

条件变量的主要操作便是发送信号(signal)和等待。发送信号操作即是通知一个或多个处于等待状态的线程,某个共享变量的状态已经改变,这些处于等待状态的线程收到通知之后便会被唤醒,唤醒之后再检查条件是否满足。等待操作是指在收到一个通知前一直处于阻塞状态。

函数 pthread_cond_signal()pthread_cond_broadcast()均可向指定的条件变量发送信号,通知一个或多个处于等待状态的线程。调用 pthread_cond_wait()函数是线程阻塞,直到收到条件变量的通知。

pthread_cond_signal()pthread_cond_broadcast()函数原型如下所示:

#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

使用这些函数需要包含头文件<pthread.h>,参数 cond 指向目标条件变量,向该条件变量发送信号。调用成功返回 0;失败将返回一个非 0 值的错误码。

pthread_cond_signal()pthread_cond_broadcast()的区别在于:二者对阻塞于 pthread_cond_wait()的多个线程对应的处理方式不同,pthread_cond_signal()函数至少能唤醒一个线程,而 pthread_cond_broadcast()函数则能唤醒所有线程。使用 pthread_cond_broadcast()函数总能产生正确的结果,唤醒所有等待状态的线程,但函数 pthread_cond_signal()会更为高效,因为它只需确保至少唤醒一个线程即可,所以如果我们的程序当中,只有一个处于等待状态的线程,使用 pthread_cond_signal()更好,具体使用哪个函数根据实际情况进行选择!

pthread_cond_wait()函数原型如下所示:

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

当程序当中使用条件变量,当判断某个条件不满足时,调用 pthread_cond_wait()函数将线程设置为等待状态(阻塞)。pthread_cond_wait()函数包含两个参数:

cond:指向需要等待的条件变量,目标条件变量;
mutex:参数 mutex 是一个 pthread_mutex_t 类型指针,指向一个互斥锁对象;

前面开头便给大家介绍了,条件变量通常是和互斥锁一起使用,因为条件的检测(条件检测通常是需要访问共享资源的)是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的。

返回值:调用成功返回 0;失败将返回一个非 0 值的错误码。

pthread_cond_wait()関数内ではmutexパラメータで指定したミューテックスを操作することになりますが、通常、条件判定やpthread_cond_wait()関数呼び出しはミューテックスによって保護されており、それ以前にスレッドがミューテックスをロックしています。関数を呼び出すpthread_cond_wait()とき、呼び出し元はミューテックスを関数に渡し、関数は呼び出し元のスレッドを条件を待っているスレッド リストに自動的に入れてから、ミューテックスのロックを解除します。ウェイクアップして戻ったときに、ミューテックスをロックしますpthread_cond_wait()。再びミューテックス。


条件変数は状態情報を保持するものではなく、アプリケーションの状態情報を渡すための単なる通信メカニズムであることに注意してください。pthread_cond_signal()指定された条件変数を呼び出してpthread_cond_broadcast()シグナルを送信するときに、条件変数を待機しているスレッドがない場合、シグナルは無視されます。
pthread_cond_broadcast() を呼び出してすべてのスレッドを同時にウェイクアップすると、ミューテックスは特定のスレッドによってのみロックされ、他のスレッドがロックの取得に失敗するとブロックされます。

この例では、
条件変数を使用してサンプル コード 3.1 を変更します。コンシューマ スレッドに消費する製品がない場合は、プロデューサが製品を生成するまで待機させます。プロデューサが製品を生成すると、コンシューマに通知します。

//3.1 生产者---消费者示例代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static pthread_mutex_t mutex; //定义互斥锁
static pthread_cond_t cond; //定义条件变量
static int g_avail = 0; //全局共享资源


/* 消费者线程 */
static void *consumer_thread(void *arg)
{
    for ( ; ; ) {
        pthread_mutex_lock(&mutex);//上锁
        while (0 >= g_avail)
            pthread_cond_wait(&cond, &mutex);//等待条件满足
        while (0 < g_avail)
        {
            g_avail--; //消费
            printf("消费:tid is %ld , g_avail = %d\n",pthread_self(),g_avail);
        }
        pthread_mutex_unlock(&mutex);//解锁
    }
    return (void *)0;
}

/* 主线程(生产者) */
int main(int argc, char *argv[])
{
    pthread_t tid;
    int ret;
    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex, NULL);
    /* 初始化条件变量 */
    pthread_cond_init(&cond, NULL);
    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, consumer_thread, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    for ( ; ; ) {
        pthread_mutex_lock(&mutex);//上锁
        g_avail++; //生产
        printf("生产:tid is %ld , g_avail = %d\n",pthread_self(),g_avail);
        pthread_mutex_unlock(&mutex);//解锁
        pthread_cond_signal(&cond);//向条件变量发送信号
    }
    exit(0);
}

グローバル変数は、g_availメインスレッドと新しいスレッドの間で共有されるリソースです。2 つのスレッドは、アクセスするまでの間、まずミューテックスをロックします。、コンシューマスレッドで、消費する製品がないと判断された場合(g_avail <= 0)、呼び出しによりpthread_cond_wait()スレッドは待機状態に陥り、条件変数を待ち、プロデューサが製造するのを待ちます。プロダクト; 呼び出し後、pthread_cond_wait()スレッドはミューテックスをブロックしてロック解除します; そしてプロデューサー スレッドでは、そのタスクはプロダクトを生成することです (g_avail++ を使用してシミュレートします)。プロダクトの生成が完了した後、呼び出してミューテックスのロックを解除し、呼び出しpthread_mutex_unlock()ますpthread_cond_signal()条件変数にシグナルを送信しますこれにより、条件変数を待機しているコンシューマ スレッドが起動されます。、ウェイクアップ後に再びミューテックスを自動的に取得し、プロダクト (g_avai-simulation) を消費します。

3.3 条件変数の判定条件

条件変数を使用する場合、関連する判定条件が存在し、通常は 1 つ以上の共有変数が関係します。例えば、サンプルコード 3.2 では、条件変数に関する判定は (0 >= g_avail) となります。注意深い読者は、このサンプル コードでは、 への呼び出しを制御するために if ステートメントの代わりに while ループを使用していることがわかります。pthread_cond_wait()なぜでしょうか?

if 文の代わりに while ループを使用する必要があります。これは、スレッドから復帰したときに、pthread_cond_wait()判定条件の状態が判断できないため、すぐに判定条件を再チェックするという一般的な設計原則です。満足できない場合は、そのままスリープして待ちます。

復帰後は pthread_cond_wait()、以下の理由により判定条件が真であるか偽であるかを判断することができません。

複数のスレッドが条件変数を待っている場合、いずれかのスレッドが先に起動してミューテックスを取得する可能性があり、最初に起動してミューテックスを取得したスレッドが共有変数を変更することで、判定条件の状態が変化する可能性があります。例えば、サンプルコード 3.2 では、コンシューマスレッドが 2 つ以上ある場合、いずれかのコンシューマスレッドがpthread_cond_wait()スレッドから復帰すると、グローバル共有変数 g_avail の値が 0 に変更され、判定条件の状態が 0 に変更されます。 true から false に変更します。

虚偽の通知が行われる可能性があります。

ケースを提供する際に、それは間違った方向に進む可能性がありますが、次のことを振り返ってください。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

//节点结构体
struct msg
{
    
    
    int num; //数据区
    struct msg *next; //链表区
};

struct msg *head = NULL;//头指针
struct msg *mp = NULL;  //节点指针

//利用宏定义的方式初始化全局的互斥锁和条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;

void *producter(void *arg)
{
    
    
    while (1)
    {
    
    
        mp = (struct msg *)malloc(sizeof(struct msg));
        mp->num = rand() % 400 + 1;
        printf("producer: %d\n", mp->num);
        
        pthread_mutex_lock(&mutex);//访问共享区域必须加锁
        mp->next = head;
        head = mp;
        pthread_mutex_unlock(&mutex);
        
        pthread_cond_signal(&has_product);//通知消费者来消费
        
        sleep(rand() % 3);
    }
    
    return NULL;
}

void *consumer(void *arg)
{
    
    
    while (1)
    {
    
    
        pthread_mutex_lock(&mutex);//访问共享区域必须加锁
        while (head == NULL)//如果共享区域没有数据,则解锁并等待条件变量
        {
    
    
            pthread_cond_wait(&has_product, &mutex);
        }
        mp = head;
        head = mp->next;
        pthread_mutex_unlock(&mutex);
        
        printf("consumer: %d\n", mp->num);
        free(mp); //释放被删除的节点内存
        mp = NULL;//并将删除的节点指针指向NULL,防止野指针
        
        sleep(rand() % 3);
    }
    
    return NULL;
}

int main(void)
{
    
    
    pthread_t ptid, ctid;
    
    //创建生产者和消费者线程
    pthread_create(&ptid, NULL, producter, NULL);
    pthread_create(&ctid, NULL, consumer, NULL);
    //主线程回收两个子线程
    pthread_join(ptid, NULL);
    pthread_join(ctid, NULL);
    
    return 0;
}

3.4 条件変数のプロパティ

上で述べたように、pthread_cond_init()条件変数を初期化するために関数が呼び出されるとき、パラメータ attr で指定される条件変数の属性を設定できます。パラメータ attr はpthread_condattr_t、条件変数のプロパティを定義する型オブジェクトを指します。もちろん、パラメータ attr が NULL に設定されている場合は、デフォルト値が条件変数のプロパティの初期化に使用されることを意味します。

条件変数のプロパティについては、ここでは詳しく説明しません。条件変数には、プロセス共有プロパティクロック プロパティの2 つのプロパティが含まれます。各属性には、対応する get メソッドと set メソッドが用意されていますが、興味のある読者は情報を参照して自分で学習することができるため、ここでは紹介しません。

4 自旋锁

自旋锁与互斥锁很相似,从本质上说也是一把锁,在访问共享资源之前对自旋锁进行上锁,在访问完成后释放自旋锁(解锁);事实上,从实现方式上来说,互斥锁是基于自旋锁来实现的,所以自旋锁相较于互斥锁更加底层。

如果在获取自旋锁时,自旋锁处于未锁定状态,那么将立即获得锁(对自旋锁上锁);如果在获取自旋锁时,自旋锁已经处于锁定状态了,那么获取锁操作将会在原地“自旋”,直到该自旋锁的持有者释放了锁

由此介绍可知,自旋锁与互斥锁相似,但是互斥锁在无法获取到锁时会让线程陷入阻塞等待状态;而自旋锁在无法获取到锁时,将会在原地“自旋”等待。“自旋”其实就是调用者一直在循环查看该自旋锁的持有者是否已经释放了锁,“自旋”一词因此得名。

自旋锁的不足之处在于:自旋锁一直占用的CPU,它在未获得锁的情况下,一直处于运行状态(自旋),所以占着 CPU,如果不能在很短的时间内获取锁,这无疑会使 CPU 效率降低。

试图对同一自旋锁加锁两次必然会导致死锁,而试图对同一互斥锁加锁两次不一定会导致死锁,原因在于互斥锁有不同的类型,当设置为 PTHREAD_MUTEX_ERRORCHECK类型时,会进行错误检查,第二次加锁会返回错误,所以不会进入死锁状态。

因此我们要谨慎使用自旋锁,自旋锁通常用于以下情况:需要保护的代码段执行时间很短,这样就会使得持有锁的线程会很快释放锁,而“自旋”等待的线程也只需等待很短的时间;在这种情况下就比较适合使用自旋锁,效率高!

综上所述,再来总结下自旋锁与互斥锁之间的区别:

  1. 实现方式上的区别:互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;
  2. オーバーヘッドの違い: ミューテックスを取得できない場合は、ロックが取得されて目覚めるまでブロック状態 (スリープ) になりますが、スピン ロックを取得できない場合は、ロックが取得されるまでその場で「スピン」します。ロックの取得; スリープとウェイクアップ オーバーヘッドが非常に高いためミューテックスのオーバーヘッドはスピン ロックのオーバーヘッドよりもはるかに高く、スピン ロックの効率はミューテックスの効率よりもはるかに高くなります。 「spin」の待機時間が長くなると、CPU の使用効率が低下するため、スピン ロックは待機時間が比較的長い状況には適していません。
  3. 使用シナリオの違い: スピン ロックはユーザー モード アプリケーションではめったに使用されず、通常はカーネル コードでより多く使用されます。スピン ロックは割り込みサービス関数で使用できますが、ミューテックスは実行割り込みでは使用できないためです。 (カーネル内でスピン ロックを使用すると、プリエンプションが自動的に禁止されます) 一度休止状態になると、割り込みサービス関数が実行されるときに CPU 使用権がアクティブに引き継がれることを意味し、元の状態に戻ることはできません。スリープ終了時のサービス関数の割り込みにより、デッドロックが発生します。

4.1 スピンロックの初期化

スピン ロックはpthread_spinlock_tデータ型で表されます。スピン ロックを定義した後、pthread_spin_init()関数で初期化する必要があります。スピン ロックが使用されなくなった場合、pthread_spin_destroy()関数が呼び出されて破棄されます。関数のプロトタイプは次のとおりです:

#include <pthread.h>
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

これら 2 つの関数を使用するには、ヘッダー ファイルをインクルードする必要があります<pthread.h>パラメータ lock は、初期化または破棄する必要があるスピン ロック オブジェクトを指し、パラメータ pshared はスピン ロックのプロセス共有属性を示します。これは次の値を取ることができます

PTHREAD_PROCESS_SHARED: 共有スピン ロック。スピン ロックは、複数のプロセスのスレッド間で共有できます。PTHREAD_PROCESS_PRIVATE
: プライベート スピン ロック。このプロセスのスレッドのみがスピン ロックを使用できます。

これら 2 つの関数は、呼び出しが成功すると 0 を返し、失敗すると 0 以外のエラー コードを返します。

4.2 Spinlock のロックとロック解除

pthread_spin_lock()関数またはpthread_spin_trylock()関数を使用してスピン ロックをロックできます。前者はロックが取得できない場合はスピンし続けます。後者は、ロックが取得できない場合はすぐにエラーが返され、エラー コードは EBUSY ですロックの方法に関係なく、スピン ロックはpthread_spin_unlock()スピン ロックを解除する機能を使用できます。その関数プロトタイプは次のとおりです。

#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

これらの関数を使用するには、ヘッダー ファイルをインクルードする必要があります<pthread.h>パラメータ lock はスピン ロック オブジェクトを指し、呼び出しが成功した場合は 0 を返し、失敗した場合は 0 以外のエラー コードを返します。

スピン ロックがロック解除されている場合、呼び出しはpthread_spin_lock()スピン ロックをロックします (ロック)。他のスレッドがスピン ロックをロックしている場合、この呼び出しは「スピン」して待機します。同じスピン ロックを使用しようとすると、2 回ロックすると必然的に行き詰まり。

使用例
サンプルコード2.1を修正し、スピンロックを使用してミューテックスを置き換え、スレッド同期を実現し、共有リソースへのアクセスを保護します。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static int loops;
static int g_count = 0;
static pthread_spinlock_t spin;//定义自旋锁

static void *new_thread_start(void *arg)
{
    int loops = *((int *)arg);
    int l_count, j;
    for (j = 0; j < loops; j++) {
        pthread_spin_lock(&spin); //自旋锁上锁
 
        l_count = g_count;
        l_count++;
        g_count = l_count;
        
        pthread_spin_unlock(&spin);//自旋锁解锁
    }
    return (void *)0;
}

int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;
    int ret;

    /* 获取用户传递的参数 */
    if (2 > argc)
        loops = 10000000; //没有传递参数默认为 1000 万次
    else
        loops = atoi(argv[1]);

    /* 初始化自旋锁(私有) */
    pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);

    /* 创建 2 个新线程 */
    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 等待线程结束 */
    ret = pthread_join(tid1, NULL);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_join(tid2, NULL);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 打印结果 */
    printf("g_count = %d\n", g_count);
    exit(0);
}

操作結果:

sundp@sundongpeng:~/workspace/IMX6ULL/Board_Drivers$ gcc test.c -lpthread
sundp@sundongpeng:~/workspace/IMX6ULL/Board_Drivers$ ./a.out
g_count = 20000000

将互斥锁替换为自旋锁之后,测试结果打印也是没有问题的,并且通过对比可以发现,替换为自旋锁之后,程序运行所耗费的时间明显变短了,说明自旋锁确实比互斥锁效率要高,但是一定要注意自旋锁所适用的场景。

5 读写锁

互斥锁或自旋锁要么是加锁状态、要么是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁有 3 种状态:读模式下的加锁状态(以下简称读加锁状态)、写模式下的加锁状态(以下简称写加锁状态)和 不加锁状态(见),一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。因此可知,读写锁比互斥锁具有更高的并行性!

stateDiagram-v2
    state if_state <>
    [*] --> 读写锁
    读写锁  --> 读模式下的读写锁
    读写锁  --> 写模式下的读写锁
    读写锁  --> 不加锁

读写锁有如下两个规则:

  1. 当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读模式加锁还是以写模式加锁)的线程都会被阻塞。
  2. 当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。

虽然各操作系统对读写锁的实现各不相同,但当读写锁处于读模式加锁状态,而这时有一个线程试图以写模式获取锁时,该线程会被阻塞;而如果另一线程以读模式获取锁,则会成功获取到锁,对共享资源进行读操作。

所以,读写锁非常适合于对共享数据读的次数远大于写的次数的情况読み取り/書き込みロックが書き込みモードでロックされている場合、書き込みモードでロックを所有できるのは一度に 1 つのスレッドだけであるため、保護されているデータは安全に変更できます。読み取り/書き込みロックが読み取りモードでロックされている場合は、保護されたデータは、読み取りモード ロックを取得した複数のスレッドによって読み取られるようになります。したがって、アプリケーション プログラムでは、読み取り/書き込みロックを使用してスレッドの同期を実現し、スレッドが共有データを読み取る必要がある場合、最初に読み取りモード ロックを取得し (読み取りモード ロックをロックし)、その後読み取りモード ロックを解放する必要があります。読み取り操作が完了した後のモード ロック モード ロック (読み取りモード ロックのロック解除); スレッドが共有データに書き込む必要がある場合、最初に書き込みモード ロックを取得し、書き込み後に書き込みモード ロックを解放する必要があります。操作が完了しました。

読み取り/書き込みロックは共有ミューテックスとも呼ばれます。読み取り/書き込みロックが読み取りモードでロックされている場合、それは共有モードでロックされていると言えます。書き込みモードでロックされている場合、排他モードでロックされていると言えます。

5.1 読み取り/書き込みロックの初期化

相互排他ロックやスピン ロックと同様に、読み取り/書き込みロックは、読み取り/書き込みロックを使用する前に初期化する必要があります。読み取り/書き込みロックはデータ型で表され、読み取り/書き込みロックの初期化にはpthread_rwlock_tマクロPTHREAD_RWLOCK_INITIALIZERまたは関数pthread_rwlock_init()。排他ロックも同じです。たとえば、PTHREAD_RWLOCK_INITIALIZER初期化にマクロを使用する場合、読み取り/書き込みロックを定義するときにマクロを初期化する必要があります。

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

pthread_rwlock_init()他のメソッドの場合は、関数を使用して初期化できます。読み取り/書き込みロックが使用されなくなったら、pthread_rwlock_destroy()関数を呼び出して破棄する必要があります。関数のプロトタイプは次のとおりです。

#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);

これら 2 つの関数を使用するには、ヘッダー ファイルも含める必要があります<pthread.h>。呼び出しが成功した場合は 0 が返され、失敗した場合は 0 以外のエラー コードが返されます。

このパラメータは、rwlock初期化または破棄する必要がある読み取り/書き込みロック オブジェクトを指します。関数の場合pthread_rwlock_init()、パラメータ attr はオブジェクトpthread_rwlockattr_t *を指す型ポインタです。データ型は、読み取り/書き込みロックの属性を定義します。パラメータ attr が NULL に設定されている場合、読み取り/書き込みロックの属性がデフォルト値に設定されていることを意味します。この場合、これは での初期化と同等です。この方法ですが、違いは、マクロを使用するとエラー チェックが行われないことです。pthread_rwlockattr_tpthread_rwlockattr_tPTHREAD_RWLOCK_INITIALIZER

読み取り/書き込みロックが使用されなくなった場合は、pthread_rwlock_destroy()関数を呼び出してそれを破棄する必要があります。

読み取り/書き込みロック初期化の使用例:

pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);

pthread_rwlock_destroy(&rwlock);

5.2 読み取り/書き込みロックのロックとロック解除

読み取り/書き込みロックを読み取りモードでロックするには関数を呼び出す必要がありpthread_rwlock_rdlock()、読み取り/書き込みロックを書き込みモードでロックするには関数
を呼び出す必要があります。pthread_rwlock_wrlock()読み取り/書き込みロックがどのようにロックされているかに関係なく、pthread_rwlock_unlock()関数を呼び出してロックを解除できます。関数のプロトタイプは次のとおりです。

#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

これらの関数を使用するには、ヘッダー ファイルをインクルードする必要があり<pthread.h>、パラメーター rwlock は読み取り/書き込みロック オブジェクトを指します。呼び出しが成功した場合は 0 を返し、失敗した場合は 0
以外のエラー コードを返します。

読み取り/書き込みロックが書き込みモードのロック状態にある場合、他のスレッド呼び出しpthread_rwlock_rdlock()またはpthread_rwlock_wrlock()関数はロックの取得に失敗するため、ブロック待機状態に陥ります。読み取り/書き込みロックが読み取りモードのロック状態にある場合は、ブロック待機状態になります。モードでは、他のスレッドはpthread_rwlock_rdlock()関数を呼び出すことでロックを正常に取得できますが、pthread_rwlock_wrlock()関数が呼び出された場合はロックを取得できないため、ブロック待機状態に陥ります。

スレッドがブロックされたくない場合は、 を呼び出して、ロックを取得できない場合にロックを試みることができpthread_rwlock_tryrdlock()ますpthread_rwlock_trywrlock()どちらの関数も、すぐにエラー コード EBUSY のエラーを返します。その関数プロトタイプは次のとおりです。

#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

パラメータ rwlock は、ロックする必要がある読み取り/書き込みロックを指します。ロックが成功すると 0 が返され、ロックが失敗すると EBUSY が返されます。

使用例

サンプル コード 5.1 は、スレッド同期を達成するための読み取り/書き込みロックの使用を示しています。グローバル変数は、g_countスレッド間で共有される変数です。メイン スレッドには、変数を読み取る 5 つのスレッドが作成されますg_countこれらは同じ関数を使用します。読み取りと出力、およびスレッド番号 (1~5); 変数​​を書き込むための 5 つのスレッドがメインスレッドにも作成され、同じ関数が使用され変数の値が関数に増加します。変数の値を20倍してスレッド番号(1~5)とともに出力します。read_threadg_countg_countwrite_threadwrite_threadg_countg_count

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static int nums[5] = {
    
    0, 1, 2, 3, 4};
static pthread_rwlock_t rwlock;//定义读写锁
static int g_count = 0;

static void *read_thread(void *arg)
{
    
    
    int number = *((int *)arg);
    int j;
    for (j = 0; j < 10; j++) {
    
    
        pthread_rwlock_rdlock(&rwlock); //以读模式获取锁
        printf("读线程<%d>, g_count=%d\n", number+1, g_count);
        pthread_rwlock_unlock(&rwlock);//解锁
        sleep(1);
    }
    return (void *)0;
}
static void *write_thread(void *arg)
{
    
    
    int number = *((int *)arg);
    int j;
    for (j = 0; j < 10; j++) {
    
    
        pthread_rwlock_wrlock(&rwlock); //以写模式获取锁
        printf("写线程<%d>, g_count=%d\n", number+1, g_count+=20);
        pthread_rwlock_unlock(&rwlock);//解锁
        sleep(1);
    }
    return (void *)0;
}

int main(int argc, char *argv[])
{
    
    
    pthread_t tid[10];
    int j;
    /* 对读写锁进行初始化 */
    pthread_rwlock_init(&rwlock, NULL);
    /* 创建 5 个读 g_count 变量的线程 */
    for (j = 0; j < 5; j++)
        pthread_create(&tid[j], NULL, read_thread, &nums[j]);
    /* 创建 5 个写 g_count 变量的线程 */
    for (j = 0; j < 5; j++)
        pthread_create(&tid[j+5], NULL, write_thread, &nums[j]);
    /* 等待线程结束 */
    for (j = 0; j < 10; j++)
        pthread_join(tid[j], NULL);//回收线程
    /* 销毁自旋锁 */
    pthread_rwlock_destroy(&rwlock);
    exit(0);
}

テストをコンパイルすると、出力される結果は次のようになります。

sundp@sundongpeng:~/workspace/IMX6ULL/Board_Drivers$ gcc test.c -lpthread
sundp@sundongpeng:~/workspace/IMX6ULL/Board_Drivers$ ./a.out
read thread<1>, g_count=0
read thread<2>, g_count=0
読み取りスレッド<5>、g_count=0
読み取りスレッド<3>、g_count=0
読み取りスレッド<4>、g_count=0
書き込みスレッド<1>、g_count=20
書き込みスレッド<2>、g_count=40
書き込みスレッド<3>、g_count=60
書き込みスレッド<5>、g_count=80
書き込みスレッド<4>、g_count=100
書き込みスレッド<2>、g_count=120
書き込みスレッド<3>、g_count=140
読み取りスレッド<5>、g_count =140
読み取りスレッド<3>、g_count=140
読み取りスレッド<1>、g_count=140
読み取りスレッド<2>、g_count=140
読み取りスレッド<4>、g_count=140
書き込みスレッド<5>、g_count=160
書き込みスレッド< 4>、g_count=180
書き込みスレッド<1>、g_count=200
読み取りスレッド<4>、g_count=200
読み取りスレッド<5>、g_count=200
読み取りスレッド<1>、g_count=200
読み取りスレッド<2>、g_count=200
読み取りスレッド<3>、g_count=200
書き込みスレッド<2>、g_count=220
書き込みスレッド<3> 、g_count=240
書き込みスレッド<4>、g_count=260
書き込みスレッド<5>、g_count=280
書き込みスレッド<1>、g_count=300読み取りスレッド
<3>、g_count=300
読み取りスレッド<1>、g_count=300
読み取りthread<5>、g_count=300
読み取りスレッド<4>、g_count=300
読み取りスレッド<2>、g_count=300

この例では、読み取り/書き込みロックの使用方法を示しますが、これはデモンストレーションのみを目的としており、実際のアプリケーション プログラミングでは、アプリケーションのシナリオに応じて読み取り/書き込みロックを使用するかどうかを選択する必要があります。

5.3 読み取り/書き込みロックのプロパティ

読み取り/書き込みロックはミューテックス ロックに似ており、属性もあります。読み取り/書き込みロックの属性はpthread_rwlockattr_tデータ型で表されます。pthread_rwlockattr_tオブジェクトを定義するときは、関数を使用して初期化する必要がありますpthread_rwlockattr_init()。初期化により、pthread_rwlockattr_tそれぞれの読み取り/書き込みが定義されます。オブジェクトによって定義されたロック属性 デフォルト値に初期化されます;pthread_rwlockattr_tオブジェクトが、pthread_rwlockattr_destroy()関数を呼び出して破棄する必要があり、その関数のプロトタイプは次のとおりです。

#include <pthread.h>
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);

パラメータ attr は、初期化または破棄する必要があるオブジェクトを指しますpthread_rwlockattr_t。関数呼び出しは正常に 0 を返し、失敗した場合は 0 以外のエラー コードを返します。

読み取り/書き込みロックには、プロセス共有属性という属性が 1 つだけあります。これは、ミューテックスおよびスピン ロックのプロセス共有属性と同じです。Linux では、読み取り/書き込みロックの共有属性を設定または取得するための対応する関数が提供されています。この関数はオブジェクトから共有プロパティを取得するpthread_rwlockattr_getpshared() ために使用され、関数はオブジェクトに共有プロパティを設定するために使用されます。その関数のプロトタイプは次のとおりです。pthread_rwlockattr_tpthread_rwlockattr_setpshared()pthread_rwlockattr_t

#include <pthread.h>
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);

関数の pthread_rwlockattr_getpshared()パラメータと戻り値:

attr: pthread_rwlockattr_t オブジェクトを指します;
pshared: pthread_rwlockattr_getpshared() を呼び出して共有属性を取得し、それをパラメータ pshared が指すメモリに保存します;
戻り値: 成功した場合は 0 を返し、成功した場合は 0 以外のエラー コードを返します。失敗。

関数のpthread_rwlockattr_setpshared()パラメータと戻り値:

attr: pthread_rwlockattr_t オブジェクトを指す;
pshared: pthread_rwlockattr_setpshared() を呼び出して読み取り/書き込みロックの共有属性を設定し、パラメーター pshared で指定された値に設定します。
パラメータ pshared の可能な値は次のとおりです:
PTHREAD_PROCESS_SHARED: 共有読み取り/書き込みロック。読み取り/書き込みロックは、複数のプロセスのスレッド間で共有できます (
PTHREAD_PROCESS_PRIVATEプライベート読み取り/書き込みロック)。このプロセスのスレッドのみが読み取り/書き込みロックを使用できます。これは、読み取り/書き込みロックの共有属性のデフォルト値です。
戻り値: 呼び出しが成功した場合は 0 を返し、失敗した場合は 0 以外のエラー コードを返します。

使用方法は次のとおりです。

pthread_rwlock_t rwlock; //定义读写锁
pthread_rwlockattr_t attr; //定义读写锁属性
/* 初始化读写锁属性对象 */
pthread_rwlockattr_init(&attr);
/* 将进程共享属性设置为 PTHREAD_PROCESS_PRIVATE */
pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_PRIVATE);
/* 初始化读写锁 */
pthread_rwlock_init(&rwlock, &attr);
......
/* 使用完之后 */
pthread_rwlock_destroy(&rwlock); //销毁读写锁
pthread_rwlockattr_destroy(&attr); //销毁读写锁属性对象

おすすめ

転載: blog.csdn.net/weixin_42581177/article/details/129804400