🚀

[C言語]マルチスレッドを触ってみる

に公開

はじめに

マルチスレッド...聞くだけでちょっと嫌になりますよね...
僕もやりたくなくて今まで触れなかったところです。
ですが、なんか大学のC言語の課題でマルチスレッドがあったのでこれを期に少しメモをしておこうと思います。

環境

  • OS: Windows 11
  • 開発環境: WSL2 - Ubuntu 24.04.2 LTS
  • コンパイラ: gcc 13.3.0

別のOSやWindows上では挙動が異なる可能性があります。

マルチスレッドとは?

スレッドとは簡単に言うと、処理の流れのようなものです。
そして、そのスレッドを1つしか使わないで処理をすることをシングルスレッドといいます。
C言語では意識せずにコーディングをすると、だいたいはシングルスレッドになると思います。

本題のマルチスレッドとは、その処理の流れではるスレッドを複数使うことです。
つまり、処理の流れが複数あるということですね。

実践

それではこれから実際にコーディングしていきましょうか!
やるぞー!

includeする

まずは、スレッドの処理に必要な関数や構造体などを使うためにヘッダファイルをincludeしましょう。

#include <pthread.h>

スレッド処理にはpthread.hを使いますので、このようなコードで大丈夫ですね!
(適宜、stdio.hとかもincludeしておきましょう)

関数を用意する

これからスレッドを作成するのですが、その前にその作成するスレッドで実行する関数を定義しておきましょう。

普通の関数とは少し違います。

void* 関数名(void* arg)
{
    // 処理
    return NULL;
}

こんな感じです。

まず、返り値の型はvoid*になります。そして、引数の型もvoid*になります。
void*を簡単に説明すると、汎用的なポインタの型でいろんな型になれるってやつです。

まあ、つまりこれは関数ポインタですね。

では、実際に関数を定義していきましょう。

void* my_thread_function(void* arg)
{
    int* num = (int*)arg;
    printf("子スレッド\targ: %d\n", *num);
    return NULL;
}

とりあえず、簡単に変数を受け取ってそれを出力するだけの関数です。
注意点として、引数はvoid*型ですのでその引数を使いたいときは別の型にキャストして使うようにしましょう。
今回だと整数値なのでint*でいいですね。

スレッドの作成

次は本題!スレッドを作成していきましょう!

スレッドをつくるには、pthread_tを使います。
一応、pthread_tの定義を見てみましょう。

/* Thread identifiers.  The structure of the attribute type is not
   exposed on purpose.  */
typedef unsigned long int pthread_t;

そうです。ぼくはなんかの構造体だと思っていたんですが、ただのunsigned long intでした。
このスレッドはPOSIX仕様というものに準拠しているらしく、このPOSIX仕様とはUNIX系のOSでプログラムの呼び出し方法などの規定らしいです(説明がざっくりで申し訳ないです)。

そして、このpthread_tはPOSIXのスレッドライブラリでスレッドを識別するIDのようなものの型らしいです。

(pthreadの'p'はPOSIXのpなのかもしれないですね...)

では、その識別子をつくっていきましょう。

pthread_t thread;

これだけで大丈夫ですね。

それではこの変数と先ほど定義した関数を使ってスレッドを作成しましょう。

使う関数は、

int pthread_create(pthread_t *__restrict __newthread,
			   const pthread_attr_t *__restrict __attr,
			   void *(*__start_routine) (void *),
			   void *__restrict __arg) __THROWNL __nonnull ((1, 3))

です。あんまりよくわからないので、どのように使うのかを書いておきますね。

pthread_create(pthread_tのポインタ,
                スレッドの属性(特になければNULLでいい),
                関数ポインタ,
                引数として渡す変数のポインタ);

こんな感じですね。
ではこれを実際に使いましょう。

if (pthread_create(&thread, NULL, my_thread_function, &value))
{
    fprintf(stderr, "スレッドの作成に失敗しました\n");
    return 1;
}

こんな感じですね。
if文を使えばエラー処理もつくることができます。

それでは実際のプログラムに実装しておきましょう。

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

void* my_thread_function(void* arg)
{
    int* num = (int*)arg;
    printf("子スレッド\targ: %d\n", *num);
    return NULL;
}

int main(void)
{
    pthread_t thread;

    int value = 42;
    if (pthread_create(&thread, NULL, my_thread_function, &value))
    {
        fprintf(stderr, "スレッドの作成に失敗しました\n");
        return 1;
    }

    return 0;
}

これでスレッドを作成し、実行することができます。
元からmain関数を実行しているスレッドとmy_thread_functionを実行するスレッドの2つに分けることができました。

ですが、このままではmy_thread_functionが終わる前にmain関数が終わってしまうかもしれません。
そのため、スレッドの終了を待つ関数があります。

int pthread_join (pthread_t __th, void **__thread_return);

このようなものです。
分かりづらいのでさっきと同じように使い方を書いておきますね。

pthread_join(スレッドの識別子, 返り値のポインタ);

このように、スレッドから返り値を取得する場合は、この第2引数に変数のアドレスを指定することで返り値を取得できます。
また、返り値がないならNULLでも大丈夫です。

それではこれも実装しましょうか。

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

void* my_thread_function(void* arg)
{
    int* num = (int*)arg;
    printf("子スレッド\targ: %d\n", *num);
    return NULL;
}

int main() {
    pthread_t thread;
    int value = 42;

    if (pthread_create(&thread, NULL, my_thread_function, &value))
    {
        fprintf(stderr, "スレッドの作成に失敗しました\n");
        return 1;
    }

    pthread_join(thread, NULL);

    printf("メインスレッド終了\n");
    return 0;
}

こんな感じで大丈夫だと思います。
なお、コンパイルするときはオプションの-pthreadをつけるようにしてください。

$ gcc main.c -pthread

このような感じですね。

では実行しましょう。

子スレッド      arg: 42
メインスレッド終了

こんな感じです。
うまくスレッド内でmainスレッドの変数を使うことができていますね。

スレッドの難しいところ

それでは、次にスレッドを扱うにおいて必ずぶつかる壁について考えていきます。

2つのスレッドで同じ変数を使う機会があると思います。
それはグローバル変数のときもあるかもしれないし、さっきのような引数としてアドレスが渡された変数かもしれない。

では、一旦、なにも考えずに2つのスレッドで同じ変数を使ってみることにしましょう。

先ほどの関数my_thread_function()を少し書き換えておきます。

void* my_thread_function(void* arg) 
{
    int* num = (int*)arg;

    *num = 10;
    usleep(1000000);

    printf("子スレッド\targ: %d\n", *num);
    return NULL;
}

先ほどのコードから、引数numの値を10にするコードを書き加えました。
つまり、main関数内のvalueの値を10にするということですね。

それと、usleep(1000000);も書きました。
これは関数名からなんとなく分かると思うのですが、そこで処理を止める関数です。そして、この関数は時間が1μ秒なので、100万μ秒で1秒になります。
そのため、このコードではそこで1秒待つということになります。

つまり、valueを10にしてから1秒待つということですね。
この 変更 -> 待つ という順番が重要です。

次に、main関数もいじりましょう。

int main() {
    pthread_t thread;
    int value = 42;

    // スレッド作成
    if (pthread_create(&thread, NULL, my_thread_function, &value)) 
    {
        fprintf(stderr, "スレッドの作成に失敗しました\n");
        return 1;
    }
    usleep(500000);
    value = 2;

    // スレッドの終了を待つ
    pthread_join(thread, NULL);
    printf("mainスレッド\tvalue: %d\n", value);

    printf("メインスレッド終了\n");
    return 0;
}

あまり細かくは説明しませんが、スレッドを作成してから0.5秒待って、値を変更し、スレッドが終了してからvalueの値を出力するというものです。

それでは、さっきのコードと組み合わせます。

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

void* my_thread_function(void* arg) 
{
    int* num = (int*)arg;

    *num = 10;
    usleep(1000000);

    printf("子スレッド\targ: %d\n", *num);
    return NULL;
}

int main() {
    pthread_t thread;
    int value = 42;

    // スレッド作成
    if (pthread_create(&thread, NULL, my_thread_function, &value)) 
    {
        fprintf(stderr, "スレッドの作成に失敗しました\n");
        return 1;
    }
    usleep(500000);
    value = 2;

    // スレッドの終了を待つ
    pthread_join(thread, NULL);
    printf("mainスレッド\tvalue: %d\n", value);

    printf("メインスレッド終了\n");
    return 0;
}

この出力なのですが、どうなると思いますか?

my_thread_function関数内では*numは10、main関数内ではvalueは2と出力されそうですよね。
では、実行しましょう。

子スレッド      arg: 2
mainスレッド    value: 2
メインスレッド終了

こんな出力になると思います。

my_thread_function関数内では*numは2と出力されているんですね。

これは、my_thread_function関数でvalueが10に変更されたあと1秒待ちますが、その間にmain関数内で0.5秒待ったあと2に書き換えられているので、my_thread_function内の値も書き変わっているんです。

このような状況を、
スレッドセーフではない
といいます。
スレッドセーフとは、「複数のスレッドが同時に同じリソースにアクセスしてもデータの整合性が保たれる状態」を指します。
今回は、以下のようなタイミングで問題が発生します:

  1. 子スレッドがvalueを10に変更。
  2. その後、1秒間待機。
  3. その間に、メインスレッドがvalueを2に変更。

この結果、子スレッドが出力する値が意図せず変更されてしまいます。

この場合は意図的にsleepしていますが、子スレッド内の処理が遅くてこのような状況が起こるかもしれないです。

スレッドセーフにするには

さーてそれではスレッドセーフにするやり方を説明していきましょう!
こっからが本題!

スレッドセーフにするには、その変数を保護して書き変わらないようにしてやる必要があります。
まずは構造体pthread_mutex_tを使います。そして、初期化する値としてPTHREAD_MUTEX_INITIALIZERを指定します。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

次に、変数をロックする関数は

pthread_mutex_lock(&mutex);

ロックしたあとはアンロックしてやります。アンロックする関数は

pthread_mutex_unlock(&mutex);

それでは、これらを使って変数をロックする方法を見てみます。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&mutex);
value = 10;
pthread_mutex_unlock(&mutex);

こんな感じで、変数を使うところでロックとアンロックをしてやればその範囲では変数が変わることはないです。

ではこれを実装していきましょう。

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* my_thread_function(void* arg) 
{
    int* num = (int*)arg;

    // スレッドセーフ
    pthread_mutex_lock(&mutex);
    *num = 10;
    usleep(1000000);
    printf("子スレッド\targ: %d\n", *num);
    pthread_mutex_unlock(&mutex);

    return NULL;
}

int main() {
    pthread_t thread;
    int value = 42;

    // スレッド作成
    if (pthread_create(&thread, NULL, my_thread_function, &value)) {
        fprintf(stderr, "スレッドの作成に失敗しました\n");
        return 1;
    }

    usleep(500000);

    // スレッドセーフ
    pthread_mutex_lock(&mutex);
    value = 2;
    pthread_mutex_unlock(&mutex);

    // スレッドの終了を待つ
    pthread_join(thread, NULL);

    printf("mainスレッド\tvalue: %d\n", value);

    printf("メインスレッド終了\n");
    return 0;
}

ここでポイントなのは、ロックする範囲は、変数の値が変わっていないところなので
出力するときも範囲に含める場合がある
ということです。

それを忘れないでおいてください。

では実行してみますね。

子スレッド      arg: 10
mainスレッド    value: 2
メインスレッド終了

このようになると思います。

これにより、子スレッドで変数は保護され、出力される値もきちんと10になっていますね。

この状態をスレッドセーフといいます。

まとめ

お疲れ様でした。
最低限は、ここまで実装できれば大丈夫でしょう。

最後にまとめると

  1. pthread.hをインクルード
  2. 関数ポインタを用意
  3. pthread_tで識別子を用意
  4. pthread_create関数でスレッド作成
  5. pthread_mutex_tを用意
  6. pthread_mutex_lock関数でロック、pthread_mutex_unlock関数でアンロックしてスレッドセーフにする

これで、マルチスレッドプログラミングの基本はばっちりです!!
ちょっとおもしろくなってきたので、もう少し調べてみようかなーと思います!

それではーお疲れ様でしたー🍎

GitHubで編集を提案

Discussion