C言語の「マルチスレッド」なんて聞くと、「うわ、専門用語…」「なんだか呪文みたい…」と身構えてしまうかもしれません。でも心配ご無用!
この記事では、マルチスレッドの「そもそも何?」という基本のキから、実際にC言語でどうやって書くのか、そして「ここだけは気をつけて!」という注意点まで詳しく解説していきます。
この記事で学べること
- マルチスレッドって何? どうして必要なの? が分かる
- C言語でマルチスレッドを使うための準備が分かる
- 基本的なマルチスレッドプログラムの書き方が分かる
- 簡単なサンプルコードを動かして、動きを体感できる
- マルチスレッドを使う上での「落とし穴」を知れる
さあ、一緒にプログラムの可能性をグッと広げましょう!
C言語におけるマルチスレッドとは?
まずは「マルチスレッド」がどういうものか、ざっくりイメージをつかみましょう。
一言でいうと、マルチスレッドは「一つのプログラムの中で、複数の処理の流れ(これをスレッドと呼びます)を同時並行的に動かす仕組み」のことです。
これまでの基本的なプログラム(シングルスレッドプログラム)は、いわば一本道。プログラムの最初から最後まで、順番に処理を実行していくイメージでした。
でもマルチスレッドなら、その一本道から複数の脇道(スレッド)が分岐して、それぞれの道で別の作業を同時に進められるんです!
[ここにシングルスレッド(一本道)とマルチスレッド(複数の脇道)を比較するイメージ図が入ります]
なぜこんな仕組みが必要になるのでしょう? 理由はいくつかあります。
- 処理の高速化
特に、最近のパソコンやスマホに搭載されている複数の計算コア(マルチコアCPU)を有効活用して、全体の処理時間を短縮できます。 - 応答性の向上
例えば、時間のかかるファイルダウンロード中に、ユーザーインターフェース(ボタン操作など)は固まらずに反応し続ける、といったことが可能になります。 - 処理の分割
プログラムの役割ごとに処理をスレッドに分けることで、プログラムの構造を整理しやすくなる場合もあります(ただし、これは少し応用的な話です)。
シングルスレッドが「一人で順番に仕事をこなす」スタイルなら、マルチスレッドは「複数人の分身(スレッド)を呼び出して、手分けして仕事を進める」スタイル、と考えると分かりやすいかもしれませんね。
スレッドの基本概念 - プロセスとの違い
スレッドとよく似た言葉に「プロセス」があります。この二つ、どう違うのでしょうか?
ざっくり言うと、プロセスは「実行中のプログラムそのもの」を指します。あなたがC言語のプログラムを実行すると、OS(オペレーティングシステム)上で一つのプロセスが生まれます。
一方、スレッドは「プロセスの中で実際に処理を実行する、より小さな単位」です。
大きな違いは、メモリ空間の扱いです。
- プロセス
基本的に、他のプロセスとは独立したメモリ空間を持ちます。他のプログラムのデータを勝手に書き換えたりはできません。 - スレッド
同じプロセス内のスレッド同士は、メモリ空間(変数など)を共有します。これが協力して作業できる理由でもあり、後述する注意点にも繋がります。
例えるなら、プロセスは「一軒家」、スレッドはその家の中にいる「住人たち」のようなものです。
住人たち(スレッド)は、リビングやキッチン(共有メモリ)を一緒に使えますが、隣の家(別プロセス)の冷蔵庫を勝手に開けることはできません。このイメージ、なんとなく掴めましたか?
C言語でマルチスレッドを使うメリット・デメリット
マルチスレッド、なんだか良さそう!と感じたかもしれませんが、物事には良い面と、ちょっと気をつけるべき面があります。ここで整理しておきましょう。
<メリット>
- 処理速度の向上
特にマルチコアCPU環境で、計算量の多い処理や、待ち時間が多い処理(ファイルI/Oやネットワーク通信など)を並行して行うことで、プログラム全体の完了時間を短縮できます。 - 応答性の改善
時間のかかる処理をバックグラウンドのスレッドに任せることで、メインの処理(例えば画面の操作受付)が待たされずに済み、ユーザー体験が向上します。
<デメリット>
- プログラムが複雑になる
複数の処理が同時に動くことを考慮する必要があるため、シングルスレッドよりも設計や実装が難しくなります。 - デバッグが難しくなる
実行するたびにスレッドの動作順序が変わる可能性があり、問題の再現や原因特定が困難になることがあります。 - 競合状態(レースコンディション)などの問題
複数のスレッドが同じデータに同時にアクセスしようとすると、予期せぬ結果を引き起こす可能性があります。これについては後で詳しく説明しますね。 - デッドロック
複数のスレッドが互いに相手の処理が終わるのを待ち続けてしまい、プログラム全体が停止してしまう状態に陥ることがあります。
「え、デメリットも結構あるじゃん…」と思ったかもしれません。
その通り!だからこそ、基本をしっかり理解して、注意点を押さえておく必要があるのです。
C言語でのマルチスレッドの基本的な書き方 (pthread)
さあ、いよいよC言語でマルチスレッドプログラムを書く方法を見ていきましょう。
C言語でマルチスレッドを扱うには、いくつかの方法がありますが、ここでは広く使われている標準的なライブラリ「POSIX Threads(ポジックス・スレッズ)」、通称「pthread(ピースレッド)」を使います。
pthreadは、LinuxやmacOSなど、多くのUnix系OSで標準的に利用できるライブラリです。(Windows環境では、標準ではpthreadは使えず、Windows APIを使うのが一般的ですが、pthreadを使えるようにするライブラリもあります。)
pthreadを使うためには、ちょっとしたお作法が必要です。
- ヘッダファイルのインクルード
プログラムの先頭で、pthread関連の関数や型定義を使うために、pthread.h
をインクルードします。
#include <pthread.h>
- コンパイル時のオプション
プログラムをコンパイルする際、多くの場合、-pthread
オプション(または-lpthread
)を付ける必要があります。これは、リンカ(プログラムの部品を繋ぎ合わせるやつ)にpthreadライブラリを使うことを教えるためです。
gcc your_program.c -o your_program -pthread
準備はOKですか? では、具体的な関数を見ていきましょう!
スレッドを作成する (pthread_create)
新しいスレッドを作り出すには、pthread_create
関数を使います。これがマルチスレッドの始まりの合図です!
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
引数がいくつかあって、ちょっと intimidating(威圧的)に見えるかもしれませんが、一つずつ見ていけば怖くありません。
pthread_t *thread
生成されたスレッドを識別するためのID(番号札のようなもの)が、このポインタが指す場所に格納されます。後でスレッドを操作するときに使います。const pthread_attr_t *attr
スレッドの属性(スタックサイズなど、細かい設定)を指定します。通常はあまり気にせず、NULL
を指定すればデフォルト設定で動きます。初心者はこちらでOK!void *(*start_routine) (void *)
これが一番重要! 新しく生成したスレッドに実行させたい関数のポインタ(関数のアドレス)を指定します。この関数が、別スレッドとして動き出す本体です。void *
が多くて難しく見えますが、「何かしらのポインタ型を引数に取り、何かしらのポインタ型を返す関数」という意味合いです。最初は「こういう型の関数を用意するんだな」と理解しておけば大丈夫。void *arg
上のstart_routine
で指定した関数に渡したい引数を指定します。引数が不要ならNULL
でOK。複数の値を渡したい場合は、構造体にまとめてそのポインタを渡す、といった工夫が必要です。
成功すると0を、失敗するとエラーコードを返します。エラー処理も本当は書くべきですが、まずは基本的な使い方をマスターしましょう!
スレッドの処理完了を待つ (pthread_join)
pthread_create
でスレッドを作りっぱなしにして、メインの処理(main
関数)が先に終わってしまうと、新しく作ったスレッドが最後まで仕事を終えられないかもしれません。
そこで、生成したスレッドの処理が終わるのを待つために使うのが pthread_join
関数です。
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
引数を見てみましょう。
pthread_t thread
どのスレッドの終了を待つかを指定します。pthread_create
で取得したスレッドIDをここに渡します。void **retval
終了したスレッドが返した値(start_routine
の戻り値)を受け取るためのポインタのポインタです。スレッドからの結果を受け取る必要がない場合は、NULL
を指定できます。初心者はこちらを使うことが多いでしょう。
pthread_join
を呼び出すと、指定したスレッドが終了するまで、呼び出し元のスレッド(通常はメインスレッド)はその場で待機します。
pthread_create
でスレッドを作ったら、基本的には対応する pthread_join
でその終了を待つ、とセットで覚えておくと良いでしょう。 これが、スレッドをきちんと後片付けするための作法の一つです。
C言語のマルチスレッドの使い方
お待たせしました! ここまで学んだ pthread_create
と pthread_join
を使って、実際に動く簡単なマルチスレッドプログラムを見てみましょう。
このプログラムでは、2つのスレッドを生成します。それぞれのスレッドは、異なるメッセージを画面に5回表示する、というシンプルな処理を行います。
これを動かしてみることで、「おお、本当に同時に動いてるっぽい!」という感覚を掴めるはずです。
サンプルプログラム
以下がサンプルコードです。コピーして、multi_thread_sample.c
のような名前で保存してみてください。
#include <stdio.h>
#include <stdlib.h> // exit() を使うため
#include <pthread.h>
#include <unistd.h> // sleep() を使うため (処理の様子を見やすくするため)
// スレッドで実行される関数
void *thread_function(void *arg) {
// void*型で渡された引数を、本来の型(char*)にキャストして受け取る
char *message = (char *)arg;
for (int i = 0; i < 5; i++) {
printf("%s (%d回目)\n", message, i + 1);
sleep(1); // 1秒待機 (他のスレッドにCPUが移る機会を作る)
}
// この関数の戻り値は使わないのでNULLを返す
return NULL;
}
int main() {
pthread_t thread1, thread2; // スレッドIDを格納する変数
const char *message1 = "Thread 1 が動いてるよ!";
const char *message2 = "Thread 2 も元気いっぱい!";
int ret1, ret2;
printf("メイン:スレッドを作成します。\n");
// スレッド1を作成 (thread_function を実行、引数は message1)
ret1 = pthread_create(&thread1, NULL, thread_function, (void*) message1);
if (ret1 != 0) {
perror("スレッド1の作成に失敗");
exit(EXIT_FAILURE);
}
// スレッド2を作成 (thread_function を実行、引数は message2)
ret2 = pthread_create(&thread2, NULL, thread_function, (void*) message2);
if (ret2 != 0) {
perror("スレッド2の作成に失敗");
// 本来は作成済みのthread1を適切に処理すべきだが、ここでは簡略化
exit(EXIT_FAILURE);
}
printf("メイン:スレッドの終了を待ちます。\n");
// スレッド1の終了を待つ
pthread_join(thread1, NULL);
printf("メイン:スレッド1が終了しました。\n");
// スレッド2の終了を待つ
pthread_join(thread2, NULL);
printf("メイン:スレッド2が終了しました。\n");
printf("メイン:全ての処理が完了しました。\n");
return 0;
}
コンパイルは、ターミナル(コマンドプロンプト)で以下のように行います。
gcc multi_thread_sample.c -o multi_thread_sample -pthread
そして実行!
./multi_thread_sample
実行結果
実行すると、おそらく次のような感じで出力が表示されるはずです(表示される順番は毎回同じとは限りません!)。
メイン:スレッドを作成します。
メイン:スレッドの終了を待ちます。
Thread 1 が動いてるよ! (1回目)
Thread 2 も元気いっぱい! (1回目)
Thread 1 が動いてるよ! (2回目)
Thread 2 も元気いっぱい! (2回目)
Thread 1 が動いてるよ! (3回目)
Thread 2 も元気いっぱい! (3回目)
Thread 1 が動いてるよ! (4回目)
Thread 2 も元気いっぱい! (4回目)
Thread 1 が動いてるよ! (5回目)
Thread 2 も元気いっぱい! (5回目)
メイン:スレッド1が終了しました。
メイン:スレッド2が終了しました。
メイン:全ての処理が完了しました。
どうでしょう? Thread 1 のメッセージと Thread 2 のメッセージが、交互に入り乱れるように表示されていませんか? これが、二つのスレッドが(見かけ上)同時に動いている証拠です。
もしかしたら、あなたの環境では Thread 1 が連続で表示された後に Thread 2 が表示されたり、その逆だったりするかもしれません。
この「実行するたびに順番が変わる可能性がある」という点が、マルチスレッドの大きな特徴であり、注意点でもあります。
サンプルプログラムの解説
コードの流れを追いかけてみましょう。
- 準備
必要なヘッダファイルをインクルードします。pthread.h
がマルチスレッド用、unistd.h
は処理の様子を見やすくするためのsleep()
関数用です。 - スレッド関数定義 (
thread_function
)
この関数が、新しく作るスレッドで実行される処理の本体です。pthread_create
から引数(ここではメッセージ文字列)をvoid*
型で受け取り、それを本来のchar*
型にキャスト(型変換)して使っています。
ループの中でメッセージを表示し、sleep(1)
で1秒待機しています。この待機を入れることで、OSが他のスレッドに処理を切り替える機会が生まれ、交互に表示されやすくなります。 main
関数開始
スレッドIDを格納する変数thread1
,thread2
と、各スレッドに渡すメッセージ文字列を用意します。- スレッド作成 (
pthread_create
)
pthread_create
を2回呼び出して、それぞれthread_function
を実行するスレッドを2つ生成します。3番目の引数で実行する関数を指定し、4番目の引数でその関数に渡すメッセージ文字列を(void*)
でキャストして渡しています。
エラーチェックも入れています(成功時は0が返る)。 - 終了待機 (
pthread_join
)
pthread_join
を2回呼び出して、生成したスレッドthread1
とthread2
が終了するのを待ちます。main
関数はここで一旦停止し、指定したスレッドが終わるまで先に進みません。
このjoin
がないと、main
関数が先に終わってしまい、スレッドの処理が中途半端になる可能性があります。 - 完了
全てのスレッドが終了したら、main
関数も終了します。
実行結果が毎回同じにならない可能性があるのは、OS(コンピュータの管理人さん)が、どのスレッドにCPU(計算する人)を使わせるかを、その時の状況に応じて判断している(スケジューリングしている)からです。
これが 非同期性 と呼ばれる性質の一つです。面白いですよね!
C言語でマルチスレッドを扱う上での注意点
さて、マルチスレッドの基本的な動かし方が分かったところで、今度は「安全に」使うための注意点についてお話しします。
マルチスレッドは強力な道具ですが、使い方を間違えると、思わぬバグ(プログラムの不具合)を生み出すことがあります。特に初心者がハマりやすいポイントを押さえておきましょう。
複数スレッドからアクセスされるデータ(共有リソース)の問題点
マルチスレッドの大きな特徴は、「同じプロセス内のスレッド同士はメモリ空間を共有する」ことでしたね。これは便利な反面、危険も伴います。
想像してみてください。複数のスレッドが、同じ変数(例えば、全体のアクセス数をカウントするグローバル変数など)を同時に書き換えようとしたらどうなるでしょう?
例えば、あるスレッドが変数の値を読み取って「+1」しようとしている瞬間に、別のスレッドが同じ変数を読み取って「+1」しようとした場合、お互いの処理が中途半端に混ざり合ってしまい、結果的に「+1」しかされなかったり、予期せぬ値になったりすることがあります。
このような、複数のスレッドが共有リソース(データなど)にアクセスするタイミングによって、結果が変わってしまう問題を「競合状態(レースコンディション)」と呼びます。 これはマルチスレッドで最も注意すべき問題の一つです。
この競合状態を防ぐためには、「あるスレッドが共有リソースを使っている間は、他のスレッドは待ってもらう」という仕組みが必要です。これを「排他制御(はいたせいぎょ)」や「同期(どうき)」と呼びます。
C言語 (pthread) では、そのための仕組みとして「Mutex(ミューテックス)」や「セマフォ」といったものが用意されています。これらを使うことで、共有リソースへのアクセスを安全に行うことができます。
ただし、これらの使い方は少し複雑になるため、今回の入門記事では深入りしません。「マルチスレッドで共有のデータを扱うときは、特別な注意が必要なんだな」ということだけ、まずはしっかり覚えておいてください。Mutexなどの詳しい使い方は、また別の機会に!
もう一つ、簡単に触れておきたいのが「デッドロック」です。
これは、複数のスレッドが、互いに相手が持っているリソース(資源)の解放を待ち続けてしまい、全員が動けなくなってしまう状態です。まるで、狭い道で車が互いに道を譲らず、にらみ合ったまま立ち往生しているようなイメージですね。これも複雑なプログラムでは起こりうる問題です。
【まとめ】C言語マルチスレッドの基本をマスターしよう
お疲れ様でした! 今回はC言語のマルチスレッドについて、その基本から簡単な実装、そして注意点までを見てきました。
ポイントを振り返ってみましょう。
- マルチスレッドは、プログラム内で複数の処理を同時に動かす仕組み。
- 高速化や応答性向上のメリットがある一方、プログラムは複雑になる。
- C言語 (pthread) では
pthread_create
でスレッドを作り、pthread_join
で終了を待つのが基本。 - 複数のスレッドが共有データにアクセスする際は「競合状態」に要注意!
サンプルコードを動かしてみて、「お、なんだか動いたぞ!」と感じられたなら、あなたはもうマルチスレッドプログラミングの第一歩を踏み出せています!素晴らしい!
最初は難しく感じるかもしれませんが、実際にコードを書いて動かしてみるのが一番の近道です。
今回学んだのは、マルチスレッドのほんの入り口です。さらに深く学ぶなら、「排他制御」「Mutex(ミューテックス)」「セマフォ」「条件変数」といったキーワードで調べてみると、より安全で高度なマルチスレッドプログラミングの世界が広がっていますよ。
この記事が、あなたのC言語学習の一助となれば幸いです。どんどんコードを書いて、プログラムの可能性を広げていってくださいね!応援しています!
【関連記事】
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。