プロセス

[C言語] pthreadの使い方とは?基本的なスレッド処理を解説

pthreadはC言語でマルチスレッド処理を行うためのPOSIX標準ライブラリです。

基本的な使い方として、pthread_create関数で新しいスレッドを生成し、実行する関数を指定します。

スレッドの終了を待つにはpthread_joinを使用します。

スレッド間の同期にはmutexや条件変数を利用し、データ競合を防ぎます。

これにより、並行処理や効率的なプログラム設計が可能となります。

pthreadとは

pthread(POSIX Threads)は、C言語におけるマルチスレッドプログラミングを実現するための標準ライブラリです。

POSIX(Portable Operating System Interface for Unix)の一部として定義されており、プラットフォーム間での互換性を持ちながらスレッドの作成や管理を行うことができます。

主な特徴

  • 軽量なスレッド管理: プロセス全体を再起動することなく、複数のスレッドを効率的に管理できます。
  • 並行処理の実現: マルチコアプロセッサを活用し、プログラムの実行速度レスポンスタイムを向上させることが可能です。
  • 豊富な同期機能: ミューテックスや条件変数など、スレッド間の競合を防ぐための同期手段が提供されています。

以下は、pthreadを使用して簡単なスレッドを作成し、メインスレッドと並行して処理を行うサンプルコードです。

#include <stdio.h>
#include <pthread.h>
// スレッドが実行する関数
void* threadFunction(void* arg) {
   printf("スレッドが実行されています。\n");
   return NULL;
}
int main() {
   pthread_t thread;
   // スレッドの作成
   if (pthread_create(&thread, NULL, threadFunction, NULL) != 0) {
       printf("スレッドの作成に失敗しました。\n");
       return 1;
   }
   // メインスレッドの処理
   printf("メインスレッドが実行されています。\n");
   // スレッドの終了を待つ
   pthread_join(thread, NULL);
   return 0;
}
メインスレッドが実行されています。
スレッドが実行されています。

この例では、pthread_create関数を使用して新しいスレッドを生成し、threadFunction関数を実行しています。

メインスレッドは自身の処理を継続しつつ、pthread_join関数で生成したスレッドの終了を待機します。

pthreadを活用することで、複数の処理を並行して実行し、プログラムの効率化や性能向上を図ることができます。

次のセクションでは、具体的なスレッドの生成方法について詳しく解説します。

スレッドの生成方法

pthreadを使用して新しいスレッドを生成するには、主にpthread_create関数を利用します。

このセクションでは、pthread_create関数の基本的な使い方と、その引数について詳しく解説します。

pthread_create関数の概要

pthread_create関数は、新しいスレッドを作成し、指定した関数を実行させるために使用されます。

関数のプロトタイプは以下の通りです:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

引数の説明

  • thread: 作成されたスレッドの識別子を格納するための変数へのポインタです。
  • attr: スレッドの属性を指定するための構造体へのポインタです。デフォルトの属性を使用する場合はNULLを指定します。
  • start_routine: スレッドが実行する関数へのポインタです。この関数はvoid*型の引数を取り、void*型の値を返します。
  • arg: start_routineに渡す引数です。複数の引数を渡したい場合は、構造体などを使用して一つのポインタにまとめます。

以下のサンプルコードでは、pthread_createを用いて新しいスレッドを生成し、別の関数printMessageを実行しています。

スレッドに渡すメッセージを引数として渡す例です。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
// スレッドが実行する関数
void* printMessage(void* arg) {
   char* message = (char*)arg;
   printf("スレッドメッセージ: %s\n", message);
   return NULL;
}
int main() {
   pthread_t thread;
   const char* message = "こんにちは、スレッド!";
   // スレッドの作成
   int result = pthread_create(&thread, NULL, printMessage, (void*)message);
   if (result != 0) {
       printf("スレッドの作成に失敗しました。\n");
       return 1;
   }
   // メインスレッドのメッセージ
   printf("メインスレッドメッセージ: スレッドが作成されました。\n");
   // スレッドの終了を待つ
   pthread_join(thread, NULL);
   return 0;
}
メインスレッドメッセージ: スレッドが作成されました。
スレッドメッセージ: こんにちは、スレッド!

サンプルコードの解説

  1. ヘッダーファイルのインクルード:
  • pthread.h: POSIXスレッドライブラリを使用するために必要です。
  • その他、標準ライブラリとしてstdio.h, stdlib.h, string.hをインクルードしています。
  1. スレッド関数printMessageの定義:
  • 引数として受け取ったポインタを文字列としてキャストし、メッセージを表示します。
  1. メイン関数内でのスレッド作成:
  • pthread_createを呼び出し、新しいスレッドを生成します。
  • 成功すると、メインスレッドは自身のメッセージを表示し、pthread_joinでスレッドの終了を待機します。

エラーハンドリング

pthread_create関数は、スレッドの作成に成功した場合は0を返し、失敗した場合はエラー番号を返します。

上記のサンプルコードでは、スレッド作成の結果をチェックし、失敗した場合にエラーメッセージを表示してプログラムを終了しています。

スレッド属性の設定(オプション)

pthread_create関数の第二引数attrを使用することで、スレッドの属性をカスタマイズすることが可能です。

例えば、スレッドのスタックサイズを変更したい場合などに利用します。

デフォルトの属性を使用する場合は、NULLを指定します。

pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 1024 * 1024); // スタックサイズを1MBに設定
pthread_create(&thread, &attr, printMessage, (void*)message);
pthread_attr_destroy(&attr); // 属性オブジェクトの破棄

上記の例では、pthread_attr_tオブジェクトを初期化し、スタックサイズを1MBに設定しています。

スレッド作成後は、属性オブジェクトを破棄することを忘れないようにしましょう。

pthread_createを正しく使用することで、効率的にマルチスレッドプログラムを構築することが可能です。

次のセクションでは、生成したスレッドの管理と終了方法について詳しく解説します。

スレッドの管理と終了

マルチスレッドプログラミングにおいて、スレッドの管理終了は非常に重要な要素です。

適切に管理し終了させることで、リソースの無駄遣い予期せぬ動作を防ぐことができます。

このセクションでは、pthreadライブラリを用いたスレッドの管理方法と終了手順について解説します。

スレッドの終了方法

スレッドを終了させる方法は主に以下の2つです:

  1. スレッド関数からの正常終了 スレッド関数内でreturn文やpthread_exit関数を用いてスレッドを終了させます。
  2. 他のスレッドからの強制終了 他のスレッドからpthread_cancel関数を用いてスレッドをキャンセルします。

ただし、これは安全性に注意が必要です。

pthread_joinによるスレッドの待機

pthread_join関数は、指定したスレッドが終了するまで呼び出し元のスレッドを待機させます。

これにより、メインスレッドが子スレッドの終了を待つことができ、リソースの適切な解放が可能になります。

pthread_join関数のプロトタイプ

int pthread_join(pthread_t thread, void **retval);
  • thread: 待機対象のスレッドの識別子。
  • retval: スレッドの終了時に返された値を受け取るためのポインタ。不要な場合はNULLを指定可能。

以下のサンプルコードでは、2つのスレッドを生成し、それぞれが異なるメッセージを表示します。

メインスレッドはpthread_joinを用いて両方のスレッドの終了を待機します。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// スレッド1が実行する関数
void* threadFunction1(void* arg) {
   printf("スレッド1が実行されています。\n");
   sleep(2); // 2秒間待機
   printf("スレッド1が終了します。\n");
   return NULL;
}
// スレッド2が実行する関数
void* threadFunction2(void* arg) {
   printf("スレッド2が実行されています。\n");
   sleep(3); // 3秒間待機
   printf("スレッド2が終了します。\n");
   return NULL;
}
int main() {
   pthread_t thread1, thread2;
   // スレッド1の作成
   if (pthread_create(&thread1, NULL, threadFunction1, NULL) != 0) {
       printf("スレッド1の作成に失敗しました。\n");
       return 1;
   }
   // スレッド2の作成
   if (pthread_create(&thread2, NULL, threadFunction2, NULL) != 0) {
       printf("スレッド2の作成に失敗しました。\n");
       return 1;
   }
   // スレッド1の終了を待機
   pthread_join(thread1, NULL);
   // スレッド2の終了を待機
   pthread_join(thread2, NULL);
   printf("全てのスレッドが終了しました。\n");
   return 0;
}
スレッド1が実行されています。
スレッド2が実行されています。
スレッド1が終了します。
スレッド2が終了します。
全てのスレッドが終了しました。

サンプルコードの解説

  1. スレッド関数の定義
  • threadFunction1threadFunction2はそれぞれ別のメッセージを表示し、sleep関数で一定時間待機後に終了します。
  1. スレッドの作成
  • pthread_createを用いて2つのスレッドを生成します。各スレッドは対応する関数を実行します。
  • スレッド作成に失敗した場合、エラーメッセージを表示してプログラムを終了します。
  1. スレッドの待機
  • pthread_joinを用いて、メインスレッドがthread1thread2の終了を待機します。
  • これにより、メインスレッドは子スレッドが全て終了するまで先に進まず、最後に「全てのスレッドが終了しました。」と表示します。

スレッドの分離状態

スレッドの分離状態を設定することで、スレッドの終了時に自動的にリソースが解放されるようにすることができます。

これにはpthread_detach関数を使用します。

pthread_detach関数の使用例

#include <stdio.h>
#include <pthread.h>
// スレッドが実行する関数
void* detachedThread(void* arg) {
   printf("デタッチされたスレッドが実行されています。\n");
   // スレッドの処理
   return NULL;
}
int main() {
   pthread_t thread;
   // スレッドの作成
   if (pthread_create(&thread, NULL, detachedThread, NULL) != 0) {
       printf("スレッドの作成に失敗しました。\n");
       return 1;
   }
   // スレッドをデタッチ状態に設定
   pthread_detach(thread);
   printf("メインスレッドが終了します。\n");
   return 0;
}
デタッチされたスレッドが実行されています。
メインスレッドが終了します。

サンプルコードの解説

  1. スレッドの作成とデタッチ
  • pthread_createでスレッドを生成し、detachedThread関数を実行します。
  • pthread_detachを用いてスレッドをデタッチ状態に設定します。これにより、pthread_joinを呼び出さずにスレッドの終了を待つ必要がなくなります。
  1. メインスレッドの終了
  • メインスレッドはスレッドの終了を待たずに「メインスレッドが終了します。」と表示してプログラムを終了します。
  • デタッチされたスレッドはバックグラウンドで実行され、メインスレッドが終了してもスレッド自体は継続して実行されます。

スレッドのキャンセル

pthread_cancel関数を使用することで、他のスレッドをキャンセル(強制終了)することができます。

ただし、スレッドのキャンセルは安全性に注意が必要であり、リソースの整合性を保つための適切な手順が求められます。

pthread_cancel関数の使用例

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 長時間実行されるスレッド関数
void* longRunningThread(void* arg) {
   printf("長時間実行されるスレッドが開始されました。\n");
   while (1) {
       // 処理を継続
   }
   return NULL;
}
int main() {
   pthread_t thread;
   // スレッドの作成
   if (pthread_create(&thread, NULL, longRunningThread, NULL) != 0) {
       printf("スレッドの作成に失敗しました。\n");
       return 1;
   }
   // 3秒後にスレッドをキャンセル
   sleep(3);
   pthread_cancel(thread);
   printf("スレッドをキャンセルしました。\n");
   // スレッドの終了を待機
   pthread_join(thread, NULL);
   return 0;
}
長時間実行されるスレッドが開始されました。
スレッドをキャンセルしました。

サンプルコードの解説

  1. スレッド関数の定義
  • longRunningThreadは無限ループを実行し、長時間実行されるスレッドをシミュレーションします。
  1. スレッドの作成とキャンセル
  • メインスレッドはpthread_createを用いてlongRunningThreadを実行するスレッドを生成します。
  • 3秒間待機後、pthread_cancelを用いてスレッドをキャンセル(強制終了)します。
  1. スレッドの終了待機
  • pthread_joinでキャンセルされたスレッドの終了を待機します。

注意点として、スレッドのキャンセルはリソースの適切な解放や整合性の保持が難しい場合があるため、可能な限り避け、スレッド自身が適切に終了する設計を心がけることが推奨されます。

スレッドの管理と終了は、マルチスレッドプログラムの信頼性と効率性を確保するために欠かせない要素です。

pthread_joinを用いてスレッドの終了を待機し、pthread_detachでリソースを自動的に解放する方法、さらにはpthread_cancelによるスレッドの強制終了など、状況に応じた適切な手法を選択することが重要です。

これにより、安全で効率的なマルチスレッドプログラムを構築することが可能となります。

スレッド間の同期

マルチスレッドプログラミングにおいて、スレッド間の同期は複数のスレッドが共有資源に安全にアクセスするために不可欠です。

適切な同期を行うことで、データ競合不整合を防ぎ、プログラムの信頼性を高めることができます。

このセクションでは、ミューテックス(mutex)条件変数(condition variable)を中心に、pthreadライブラリを用いたスレッド間の同期方法について解説します。

ミューテックス(Mutex)

ミューテックスは、複数のスレッドが同時に共有資源にアクセスするのを防ぐための排他制御手段です。

ミューテックスを使用することで、一度に一つのスレッドだけが特定のコードブロックを実行できるようになります。

ミューテックスの基本的な使用方法

  1. ミューテックスの初期化 ミューテックス変数を宣言し、pthread_mutex_init関数で初期化します。

または、静的に初期化する場合はPTHREAD_MUTEX_INITIALIZERを使用します。

  1. ロックの取得 共有資源にアクセスする前に、pthread_mutex_lock関数でミューテックスをロックします。
  2. 共有資源の操作 ミューテックスがロックされた状態で、共有資源に安全にアクセス・操作します。
  3. ロックの解放 操作が完了したら、pthread_mutex_unlock関数でミューテックスを解放します。

以下のサンプルコードでは、複数のスレッドがカウンターをインクリメントする際にミューテックスを使用して同期を行っています。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
// カウンターの共有変数
int counter = 0;
// ミューテックスの宣言と初期化
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
// スレッドが実行する関数
void* incrementCounter(void* arg) {
   for (int i = 0; i < 100000; i++) {
       // ミューテックスをロック
       pthread_mutex_lock(&counter_mutex);
       // 共有変数のインクリメント
       counter++;
       // ミューテックスをアンロック
       pthread_mutex_unlock(&counter_mutex);
   }
   return NULL;
}
int main() {
   pthread_t threads[10];
   // 10個のスレッドを作成
   for (int i = 0; i < 10; i++) {
       if (pthread_create(&threads[i], NULL, incrementCounter, NULL) != 0) {
           printf("スレッドの作成に失敗しました。\n");
           return 1;
       }
   }
   // 全てのスレッドの終了を待機
   for (int i = 0; i < 10; i++) {
       pthread_join(threads[i], NULL);
   }
   // 最終的なカウンターの値を表示
   printf("最終的なカウンターの値: %d\n", counter);
   // ミューテックスの破棄
   pthread_mutex_destroy(&counter_mutex);
   return 0;
}
最終的なカウンターの値: 1000000

サンプルコードの解説

  1. ミューテックスの初期化 pthread_mutex_t counter_mutexPTHREAD_MUTEX_INITIALIZERで静的に初期化しています。
  2. スレッド関数incrementCounter 各スレッドはループ内でカウンターを100,000回インクリメントします。

インクリメント時にはミューテックスをロックし、操作後にアンロックすることで、排他制御を実現しています。

  1. スレッドの作成と待機 メイン関数では10個のスレッドを生成し、全てのスレッドが終了するまでpthread_joinで待機します。
  2. 結果の表示とミューテックスの破棄 全スレッドのインクリメントが完了した後、カウンターの最終値を表示し、ミューテックスを破棄します。

条件変数(Condition Variable)

条件変数は、スレッド間で特定の条件が満たされるまで待機させたり、他のスレッドに条件の変化を通知したりするための同期手段です。

条件変数を使用することで、スレッド間の協調動作を効率的に実現できます。

条件変数の基本的な使用方法

  1. 条件変数とミューテックスの初期化 条件変数をpthread_cond_initで初期化し、ミューテックスと組み合わせて使用します。
  2. 待機 条件が満たされるまでpthread_cond_wait関数で待機します。

この際、待機中はミューテックスが解放されます。

  1. 通知 条件が満たされた際に、pthread_cond_signalまたはpthread_cond_broadcast関数で待機中のスレッドに通知します。
  2. 破棄 使用後はpthread_cond_destroypthread_mutex_destroyで条件変数とミューテックスを破棄します。

以下のサンプルコードでは、生産者-消費者問題を条件変数を用いて解決しています。

生産者スレッドがデータを生成し、消費者スレッドがデータを消費します。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#define BUFFER_SIZE 5
// バッファ
int buffer[BUFFER_SIZE];
int count = 0;
// ミューテックスと条件変数の宣言
pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
// 生産者スレッドの関数
void* producer(void* arg) {
   for (int i = 1; i <= 10; i++) {
       // ミューテックスをロック
       pthread_mutex_lock(&buffer_mutex);
       // バッファが満杯の場合、空になるまで待機
       while (count == BUFFER_SIZE) {
           pthread_cond_wait(¬_full, &buffer_mutex);
       }
       // データをバッファに追加
       buffer[count++] = i;
       printf("生産者がデータ %d を生成しました。バッファのサイズ: %d\n", i, count);
       // 消費者に通知
       pthread_cond_signal(¬_empty);
       // ミューテックスをアンロック
       pthread_mutex_unlock(&buffer_mutex);
       sleep(1); // 生産の遅延をシミュレーション
   }
   return NULL;
}
// 消費者スレッドの関数
void* consumer(void* arg) {
   for (int i = 1; i <= 10; i++) {
       // ミューテックスをロック
       pthread_mutex_lock(&buffer_mutex);
       // バッファが空の場合、データが入るまで待機
       while (count == 0) {
           pthread_cond_wait(¬_empty, &buffer_mutex);
       }
       // データをバッファから消費
       int data = buffer[--count];
       printf("消費者がデータ %d を消費しました。バッファのサイズ: %d\n", data, count);
       // 生産者に通知
       pthread_cond_signal(¬_full);
       // ミューテックスをアンロック
       pthread_mutex_unlock(&buffer_mutex);
       sleep(2); // 消費の遅延をシミュレーション
   }
   return NULL;
}
int main() {
   pthread_t producer_thread, consumer_thread;
   // 生産者スレッドの作成
   if (pthread_create(&producer_thread, NULL, producer, NULL) != 0) {
       printf("生産者スレッドの作成に失敗しました。\n");
       return 1;
   }
   // 消費者スレッドの作成
   if (pthread_create(&consumer_thread, NULL, consumer, NULL) != 0) {
       printf("消費者スレッドの作成に失敗しました。\n");
       return 1;
   }
   // スレッドの終了を待機
   pthread_join(producer_thread, NULL);
   pthread_join(consumer_thread, NULL);
   // ミューテックスと条件変数の破棄
   pthread_mutex_destroy(&buffer_mutex);
   pthread_cond_destroy(¬_full);
   pthread_cond_destroy(¬_empty);
   printf("生産者と消費者の処理が完了しました。\n");
   return 0;
}
生産者がデータ 1 を生成しました。バッファのサイズ: 1
消費者がデータ 1 を消費しました。バッファのサイズ: 0
生産者がデータ 2 を生成しました。バッファのサイズ: 1
生産者がデータ 3 を生成しました。バッファのサイズ: 2
消費者がデータ 3 を消費しました。バッファのサイズ: 1
生産者がデータ 4 を生成しました。バッファのサイズ: 2
生産者がデータ 5 を生成しました。バッファのサイズ: 3
消費者がデータ 5 を消費しました。バッファのサイズ: 2
...
生産者と消費者の処理が完了しました。

サンプルコードの解説

  1. ミューテックスと条件変数の初期化
  • buffer_mutexPTHREAD_MUTEX_INITIALIZERで初期化し、条件変数not_fullnot_emptyをそれぞれ初期化しています。
  1. 生産者スレッド
  • ループ内でデータを生成し、バッファに追加します。
  • バッファが満杯の場合、pthread_cond_waitnot_full条件が満たされるまで待機します。
  • データを追加後、not_empty条件を通知して消費者にデータの存在を知らせます。
  1. 消費者スレッド
  • ループ内でバッファからデータを消費します。
  • バッファが空の場合、pthread_cond_waitnot_empty条件が満たされるまで待機します。
  • データを消費後、not_full条件を通知して生産者にバッファに空きができたことを知らせます。
  1. スレッドの作成と待機
  • 生産者と消費者のスレッドをそれぞれ生成し、pthread_joinで終了を待機します。
  1. リソースの破棄
  • ミューテックスと条件変数をpthread_mutex_destroyおよびpthread_cond_destroyで破棄します。

セマフォ(Semaphore)(オプション)

セマフォは、一定数のリソースに対するアクセスを制御するための同期手段です。

pthreadライブラリではなく、POSIXセマフォを利用する場合はsemaphore.hをインクルードし、sem_initsem_waitsem_postなどの関数を使用します。

ここでは、基本的なミューテックスと条件変数に焦点を当てていますが、セマフォも有用な同期手段の一つです。

サンプルコード:ミューテックスと条件変数の組み合わせ

以下のサンプルコードでは、ミューテックスと条件変数を組み合わせて使用し、複数のスレッド間で安全にデータを共有・操作しています。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#define MAX_RESOURCES 3
int available_resources = MAX_RESOURCES;
// ミューテックスと条件変数の宣言
pthread_mutex_t resource_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t resource_cond = PTHREAD_COND_INITIALIZER;
// リソースを取得する関数
void acquire_resources(int thread_id) {
   pthread_mutex_lock(&resource_mutex);
   while (available_resources < 1) {
       printf("スレッド %d がリソースの解放を待っています。\n", thread_id);
       pthread_cond_wait(&resource_cond, &resource_mutex);
   }
   available_resources--;
   printf("スレッド %d がリソースを取得しました。利用可能なリソース: %d\n", thread_id, available_resources);
   pthread_mutex_unlock(&resource_mutex);
}
// リソースを解放する関数
void release_resources(int thread_id) {
   pthread_mutex_lock(&resource_mutex);
   available_resources++;
   printf("スレッド %d がリソースを解放しました。利用可能なリソース: %d\n", thread_id, available_resources);
   pthread_cond_signal(&resource_cond);
   pthread_mutex_unlock(&resource_mutex);
}
// スレッドが実行する関数
void* threadFunction(void* arg) {
   int thread_id = *(int*)arg;
   acquire_resources(thread_id);
   // リソースを使用中
   printf("スレッド %d がリソースを使用中です。\n", thread_id);
   sleep(2); // リソース使用のシミュレーション
   release_resources(thread_id);
   return NULL;
}
int main() {
   pthread_t threads[5];
   int thread_ids[5];
   // 5つのスレッドを作成
   for (int i = 0; i < 5; i++) {
       thread_ids[i] = i + 1;
       if (pthread_create(&threads[i], NULL, threadFunction, &thread_ids[i]) != 0) {
           printf("スレッド %d の作成に失敗しました。\n", i + 1);
           return 1;
       }
   }
   // 全てのスレッドの終了を待機
   for (int i = 0; i < 5; i++) {
       pthread_join(threads[i], NULL);
   }
   // ミューテックスと条件変数の破棄
   pthread_mutex_destroy(&resource_mutex);
   pthread_cond_destroy(&resource_cond);
   printf("全てのスレッドが終了しました。\n");
   return 0;
}
スレッド 1 がリソースを取得しました。利用可能なリソース: 2
スレッド 1 がリソースを使用中です。
スレッド 2 がリソースを取得しました。利用可能なリソース: 1
スレッド 2 がリソースを使用中です。
スレッド 3 がリソースを取得しました。利用可能なリソース: 0
スレッド 3 がリソースを使用中です。
スレッド 4 がリソースの解放を待っています。
スレッド 5 がリソースの解放を待っています。
スレッド 1 がリソースを解放しました。利用可能なリソース: 1
スレッド 4 がリソースを取得しました。利用可能なリソース: 0
スレッド 4 がリソースを使用中です。
スレッド 2 がリソースを解放しました。利用可能なリソース: 1
スレッド 5 がリソースを取得しました。利用可能なリソース: 0
スレッド 5 がリソースを使用中です。
スレッド 3 がリソースを解放しました。利用可能なリソース: 1
スレッド 4 がリソースを解放しました。利用可能なリソース: 2
スレッド 5 がリソースを解放しました。利用可能なリソース: 3
全てのスレッドが終了しました。

サンプルコードの解説

  1. ミューテックスと条件変数の初期化
  • resource_mutexPTHREAD_MUTEX_INITIALIZERで初期化し、resource_condを条件変数として初期化しています。
  1. リソースの取得と解放
  • acquire_resources関数では、利用可能なリソースが1未満の場合、条件変数resource_condで通知があるまで待機します。リソースが利用可能になったら取得し、利用可能なリソース数を減少させます。
  • release_resources関数では、リソースを解放し、条件変数resource_condに通知を送ります。
  1. スレッド関数threadFunction
  • 各スレッドはリソースを取得し、リソースを使用中(sleepでシミュレーション)にし、その後リソースを解放します。
  1. スレッドの作成と待機
  • メイン関数では5つのスレッドを生成し、全てのスレッドが終了するまでpthread_joinで待機します。
  1. リソースの破棄
  • ミューテックスと条件変数をpthread_mutex_destroyおよびpthread_cond_destroyで破棄します。

スレッド間の同期のベストプラクティス

  • ロックの粒度を適切に設定する

必要以上に大きな範囲でミューテックスをロックしないようにし、デッドロックを防ぎます。

  • 条件変数の使用時にミューテックスを正しく管理する

pthread_cond_waitはミューテックスを自動的に解放し、再取得するため、条件のチェックと待機をループ内で行うことが推奨されます。

  • リソースの解放を忘れない

スレッド終了時やプログラム終了時にミューテックスや条件変数を適切に破棄します。

  • デッドロックの回避

複数のミューテックスを使用する場合、一貫した順序でロックを取得するなどして、デッドロックを防止します。

スレッド間の同期を適切に行うことで、安全で効率的なマルチスレッドプログラムを構築することが可能です。

ミューテックスと条件変数を理解し、状況に応じて適切に活用しましょう。

次のセクションでは、実践的なpthreadの活用例について詳しく解説します。

実践的なpthreadの活用例

実際のアプリケーションにおいて、pthreadを活用することで、マルチスレッドによる効率的な並列処理を実現できます。

このセクションでは、配列の並列和算出を例に、pthreadを用いた実践的な活用方法を解説します。

配列の並列和算出

大量のデータを扱う際、配列の要素を効率的に処理するために、複数のスレッドを利用して並列に計算を行うことができます。

以下のサンプルコードでは、配列を複数の部分に分割し、それぞれの部分を異なるスレッドで処理して部分和を計算し、最後に全体の和を求めます。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#define ARRAY_SIZE 1000000
#define THREAD_COUNT 4
// 配列
int array[ARRAY_SIZE];
// 部分和を格納する配列
long long partial_sums[THREAD_COUNT];
// スレッドが実行する関数
void* compute_partial_sum(void* arg) {
   int thread_id = *(int*)arg;
   int start = (ARRAY_SIZE / THREAD_COUNT) * thread_id;
   int end = (thread_id == THREAD_COUNT - 1) ? ARRAY_SIZE : start + (ARRAY_SIZE / THREAD_COUNT);
   long long sum = 0;
   for (int i = start; i < end; i++) {
       sum += array[i];
   }
   partial_sums[thread_id] = sum;
   printf("スレッド %d が部分和 %lld を計算しました。\n", thread_id, sum);
   return NULL;
}
int main() {
   pthread_t threads[THREAD_COUNT];
   int thread_ids[THREAD_COUNT];
   // 配列にデータを初期化
   for (int i = 0; i < ARRAY_SIZE; i++) {
       array[i] = 1; // 各要素を1に設定
   }
   // スレッドの作成
   for (int i = 0; i < THREAD_COUNT; i++) {
       thread_ids[i] = i;
       if (pthread_create(&threads[i], NULL, compute_partial_sum, &thread_ids[i]) != 0) {
           printf("スレッド %d の作成に失敗しました。\n", i);
           return 1;
       }
   }
   // スレッドの終了を待機
   for (int i = 0; i < THREAD_COUNT; i++) {
       pthread_join(threads[i], NULL);
   }
   // 全体の和を計算
   long long total_sum = 0;
   for (int i = 0; i < THREAD_COUNT; i++) {
       total_sum += partial_sums[i];
   }
   printf("全体のカウンターの値: %lld\n", total_sum);
   return 0;
}
スレッド 0 が部分和 250000 を計算しました。
スレッド 1 が部分和 250000 を計算しました。
スレッド 2 が部分和 250000 を計算しました。
スレッド 3 が部分和 250000 を計算しました。
全体のカウンターの値: 1000000

サンプルコードの解説

  1. 配列の初期化 array配列の各要素を1で初期化しています。

これにより、全体の和が配列のサイズと同じ1,000,000になることを確認できます。

  1. 部分和の計算 compute_partial_sum関数では、各スレッドが担当する配列の範囲を計算し、その範囲内の要素を合計しています。

計算結果はpartial_sums配列に保存され、各スレッドが計算した部分和を表示します。

  1. スレッドの作成と待機 pthread_create関数を用いて4つのスレッドを生成し、それぞれがcompute_partial_sum関数を実行します。

pthread_join関数で全てのスレッドの終了を待機し、全体の和を計算します。

  1. 全体の和の計算 各スレッドが計算した部分和をpartial_sumsから取得し、全体の和をtotal_sumとして計算・表示します。

スレッド間でのデータ共有と同期

上記の例では、スレッド間でpartial_sums配列を共有しています。

この場合、各スレッドが独立したインデックスにのみアクセスするため、データ競合の心配はありません。

しかし、より複雑なデータ共有を行う場合には、ミューテックス条件変数を用いて適切な同期を行う必要があります。

並列処理の利点

  • 処理速度の向上: 複数のスレッドが並行して処理を行うことで、計算時間を短縮できます。
  • 効率的なリソース利用: マルチコアプロセッサを活用し、CPUの使用率を最大化できます。
  • スケーラビリティ: 処理の負荷に応じてスレッド数を調整することで、柔軟なシステム設計が可能です。

注意点

  • スレッドのオーバーヘッド: スレッドの作成や管理にはコストがかかるため、処理の粒度が細かすぎると逆効果になる場合があります。
  • データ競合の回避: 共有データへのアクセス時には適切な同期を行わないと、データの不整合が発生する可能性があります。
  • デッドロックの防止: 複数のリソースを扱う場合、一貫したロックの順序を保つなどして、デッドロックを回避する設計が必要です。

pthreadを活用した並列処理は、パフォーマンス向上効率的なリソース管理に大きく寄与します。

適切な同期手段を組み合わせることで、信頼性の高いマルチスレッドアプリケーションを構築することが可能です。

次のセクションでは、さらなる高度な活用例について解説します。

まとめ

本記事では、C言語におけるpthreadライブラリを活用したマルチスレッドプログラミングの基本から実践的な例まで詳しく解説しました。

スレッドの生成や管理、同期方法を理解し、効率的な並列処理を実現する手法を紹介しました。

これらの内容を踏まえて、ぜひ自身のプロジェクトにpthreadを導入し、プログラムの性能向上に取り組んでみてください。

関連記事

Back to top button
目次へ