pthreadとmutexを使い、データの競合を起こさずに並列処理をする方法について調べてみました。
まずはptreadを用いた並列処理のサンプルです。
2つのスレッドでそれぞれ1万回ずつインクリメントすることで最終的にcntの値を20000にします。
#include <stdio.h>
#include <pthread.h>
int cnt = 0;
void *routine(void *p)
{
for (int i = 0; i < 10000; i++)
cnt++;
return (NULL);
}
int main(void)
{
pthread_t p1, p2;
// 2つのスレッドで並列処理する
pthread_create(&p1, NULL, &routine, NULL);
pthread_create(&p2, NULL, &routine, NULL);
// 終了するまで待つ
pthread_join(p1, NULL);
pthread_join(p2, NULL);
printf("cnt -> %d\n", cnt);
}
実行結果
$ gcc test.c
$ ./a.out
cnt -> 19677
$ ./a.out
cnt -> 15331
$ ./a.out
cnt -> 12060
おかしいですね。
結果が実行するたびに変わってしまいます。
実はこのままだとデータの競合が起きてしまい、正しい結果が得られません。
そのため、mutexを使うことで同時アクセスを防ぎます。
#include <stdio.h>
#include <pthread.h>
int cnt = 0;
pthread_mutex_t mutex;
void *routine(void *p)
{
for (int i = 0; i < 10000; i++)
{
pthread_mutex_lock(&mutex); //lockして同時アクセスを防ぐ
cnt++;
pthread_mutex_unlock(&mutex);
}
return (NULL);
}
int main(void)
{
pthread_t p1, p2;
pthread_mutex_init(&mutex, NULL);
// 2つのスレッドで並列処理する
pthread_create(&p1, NULL, &routine, NULL);
pthread_create(&p2, NULL, &routine, NULL);
// 終了するまで待つ
pthread_join(p1, NULL);
pthread_join(p2, NULL);
pthread_mutex_destroy(&mutex);
printf("cnt -> %d\n", cnt);
}
実行結果
$ gcc test.c
$ ./a.out
cnt -> 20000
$ ./a.out
cnt -> 20000
$ ./a.out
cnt -> 20000
無事に正しい結果が得られました。
mutex_lockをすることで同時に別のスレッドがアクセスするのを防いでくれるみたいですね。
ちなみに値の変更だけではなく、参照する際もロックが必要です。
そのため次のような条件文でアクセスする際も必要となります。
#include <stdio.h>
#include <pthread.h>
int cnt = 0;
pthread_mutex_t mutex;
void *routine(void *p)
{
for (int i = 0; i < 10000; i++)
{
pthread_mutex_lock(&mutex); // lockして同時アクセスを防ぐ
if (cnt == 10000) // 参照する際もlockが必要
{
pthread_mutex_unlock(&mutex);
break ;
}
cnt++;
pthread_mutex_unlock(&mutex);
}
return (NULL);
}
int main(void)
{
pthread_t p1, p2;
pthread_mutex_init(&mutex, NULL);
// 2つのスレッドで並列処理する
pthread_create(&p1, NULL, &routine, NULL);
pthread_create(&p2, NULL, &routine, NULL);
// 終了するまで待つ
pthread_join(p1, NULL);
pthread_join(p2, NULL);
pthread_mutex_destroy(&mutex);
printf("cnt -> %d\n", cnt);
}
実行結果
$ gcc test.c
$ ./a.out
cnt -> 10000
$ ./a.out
cnt -> 10000
$ ./a.out
cnt -> 10000
正しく10000で処理を終了できましたね。
まとめ
無事安全に並列処理を実行することができました。
gcc -fsanitize=threadでコンパイルしてあげると、データ競合のリスクがある際にエラーを出してくれるので便利です。
共用の変数にアクセスする際はロックを忘れないようにしたいですね。