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

C言語でマルチスレッドプログラミングを行う際に使用されるのが、POSIXスレッドライブラリ、通称です。

このライブラリを使用することで、複数のスレッドを生成し、並行して処理を実行することが可能になります。

スレッドの生成には関数を使用し、スレッドの終了を待つには関数を用います。

また、スレッド間でのデータ競合を防ぐために、を用いたミューテックスロックが利用されます。

これにより、スレッドセーフなプログラムを実現できます。

この記事でわかること
  • pthreadの基本的な使い方とスレッドの作成方法
  • スレッドの同期におけるミューテックスや条件変数の利用法
  • スレッドの安全性を確保するための競合状態やデッドロックの回避策
  • マルチスレッドによる並列計算やサーバーの実装例
  • スレッドプールの構築方法とその利点

目次から探す

pthreadとは何か

pthreadの概要

pthread(POSIX threads)は、POSIX標準に基づいたスレッドを扱うためのライブラリです。

C言語でマルチスレッドプログラミングを行う際に使用され、スレッドの生成、管理、同期を行うためのAPIを提供します。

pthreadを利用することで、複数のスレッドを並行して実行し、プログラムのパフォーマンスを向上させることが可能です。

スレッドとプロセスの違い

スレッドとプロセスは、コンピュータプログラムの実行単位ですが、いくつかの違いがあります。

スクロールできます
特徴スレッドプロセス
メモリ空間共有独立
作成コスト低い高い
通信方法共有メモリIPC(プロセス間通信)
独立性低い高い
  • メモリ空間: スレッドは同じプロセス内でメモリ空間を共有しますが、プロセスは独立したメモリ空間を持ちます。
  • 作成コスト: スレッドの作成はプロセスよりも低コストで行えます。
  • 通信方法: スレッド間の通信は共有メモリを通じて行われますが、プロセス間ではIPCが必要です。
  • 独立性: スレッドはプロセス内で動作するため、プロセスに比べて独立性が低いです。

pthreadの利点と用途

pthreadを使用することで、以下のような利点があります。

  • 効率的なリソース利用: スレッドはプロセスに比べて軽量で、リソースの利用効率が高いです。
  • 並列処理の実現: 複数のスレッドを同時に実行することで、プログラムの並列処理を実現できます。
  • 応答性の向上: スレッドを利用することで、ユーザーインターフェースの応答性を向上させることができます。

pthreadは、以下のような用途で広く利用されています。

  • サーバーアプリケーション: クライアントからのリクエストを並行して処理するために使用されます。
  • リアルタイムシステム: タスクの並行実行が求められるリアルタイムシステムで活用されます。
  • 科学技術計算: 大規模な計算を並列化することで、計算時間を短縮します。

pthreadの基本的な使い方

pthreadライブラリのインクルード

pthreadを使用するには、プログラムの先頭でpthreadライブラリをインクルードする必要があります。

以下のように#includeディレクティブを使用します。

#include <pthread.h>

このヘッダーファイルには、スレッドの作成や管理に必要な関数やデータ型が定義されています。

スレッドの作成と終了

スレッドを作成するには、pthread_create関数を使用します。

この関数は、新しいスレッドを生成し、指定された関数を実行します。

以下は、スレッドを作成する基本的な例です。

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

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

スレッドの終了を待つために、pthread_join関数を使用しています。

スレッドの識別子と属性

スレッドを管理するために、スレッド識別子pthread_t型が使用されます。

スレッド識別子は、スレッドの作成時に取得され、スレッドの操作に使用されます。

スレッドの属性を設定するには、pthread_attr_t型を使用します。

スレッドの属性には、スレッドの分離状態やスタックサイズなどがあります。

以下は、スレッド属性を設定する例です。

#include <pthread.h>
#include <stdio.h>
void* threadFunction(void* arg) {
    printf("スレッドが実行されています\n");
    return NULL;
}
int main() {
    pthread_t thread;
    pthread_attr_t attr;
    // スレッド属性の初期化
    pthread_attr_init(&attr);
    // スレッドの分離属性を設定
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    // スレッドの作成
    if (pthread_create(&thread, &attr, threadFunction, NULL) != 0) {
        perror("スレッドの作成に失敗しました");
        return 1;
    }
    // スレッド属性の破棄
    pthread_attr_destroy(&attr);
    return 0;
}

この例では、スレッドを分離状態で作成しています。

分離状態のスレッドは、pthread_joinを使用せずに終了します。

スレッドの結合と分離

スレッドの結合(join)とは、スレッドの終了を待つことを指します。

pthread_join関数を使用して、指定したスレッドが終了するまで待機します。

これにより、スレッドのリソースが適切に解放されます。

一方、スレッドの分離(detach)とは、スレッドが終了したときに自動的にリソースを解放することを指します。

pthread_detach関数を使用して、スレッドを分離状態に設定できます。

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

この例では、スレッドを分離状態に設定し、メインスレッドが終了してもスレッドのリソースが自動的に解放されるようにしています。

スレッドの同期

スレッドの同期は、複数のスレッドが共有リソースにアクセスする際に、データの整合性を保つために重要です。

同期を適切に行うことで、競合状態やデッドロックを防ぐことができます。

ミューテックスの使用

ミューテックス(mutex)は、排他制御を行うための同期プリミティブです。

ミューテックスを使用することで、複数のスレッドが同時に共有リソースにアクセスすることを防ぎます。

以下は、ミューテックスを使用した例です。

#include <pthread.h>
#include <stdio.h>
int sharedResource = 0;
pthread_mutex_t mutex;
void* threadFunction(void* arg) {
    pthread_mutex_lock(&mutex); // ミューテックスをロック
    sharedResource++;
    printf("共有リソースの値: %d\n", sharedResource);
    pthread_mutex_unlock(&mutex); // ミューテックスをアンロック
    return NULL;
}
int main() {
    pthread_t thread1, thread2;
    pthread_mutex_init(&mutex, NULL); // ミューテックスの初期化
    pthread_create(&thread1, NULL, threadFunction, NULL);
    pthread_create(&thread2, NULL, threadFunction, NULL);
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    pthread_mutex_destroy(&mutex); // ミューテックスの破棄
    return 0;
}

この例では、pthread_mutex_lockpthread_mutex_unlockを使用して、共有リソースへのアクセスを制御しています。

これにより、同時に複数のスレッドがsharedResourceを変更することを防ぎます。

条件変数の利用

条件変数は、スレッド間の通信を行うための同期プリミティブです。

条件変数を使用することで、ある条件が満たされるまでスレッドを待機させることができます。

以下は、条件変数を使用した例です。

#include <pthread.h>
#include <stdio.h>
int ready = 0;
pthread_mutex_t mutex;
pthread_cond_t cond;
void* threadFunction(void* arg) {
    pthread_mutex_lock(&mutex);
    while (!ready) {
        pthread_cond_wait(&cond, &mutex); // 条件変数で待機
    }
    printf("条件が満たされました\n");
    pthread_mutex_unlock(&mutex);
    return NULL;
}
int main() {
    pthread_t thread;
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);
    pthread_create(&thread, NULL, threadFunction, NULL);
    pthread_mutex_lock(&mutex);
    ready = 1;
    pthread_cond_signal(&cond); // 条件変数をシグナル
    pthread_mutex_unlock(&mutex);
    pthread_join(thread, NULL);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

この例では、pthread_cond_waitを使用してスレッドを待機させ、pthread_cond_signalで条件が満たされたことを通知しています。

バリアの活用

バリアは、複数のスレッドが特定のポイントで同期するためのプリミティブです。

すべてのスレッドがバリアに到達するまで待機し、全スレッドが到達した時点で次の処理に進みます。

以下は、バリアを使用した例です。

#include <pthread.h>
#include <stdio.h>
#define NUM_THREADS 3
pthread_barrier_t barrier;
void* threadFunction(void* arg) {
    printf("スレッドがバリアに到達しました\n");
    pthread_barrier_wait(&barrier); // バリアで待機
    printf("すべてのスレッドがバリアを通過しました\n");
    return NULL;
}
int main() {
    pthread_t threads[NUM_THREADS];
    pthread_barrier_init(&barrier, NULL, NUM_THREADS); // バリアの初期化
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_create(&threads[i], NULL, threadFunction, NULL);
    }
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }
    pthread_barrier_destroy(&barrier); // バリアの破棄
    return 0;
}

この例では、pthread_barrier_waitを使用して、すべてのスレッドがバリアに到達するまで待機し、全スレッドが到達した後に次の処理に進んでいます。

スレッドの安全性

スレッドの安全性を確保することは、マルチスレッドプログラミングにおいて非常に重要です。

スレッドの安全性を確保することで、プログラムの予期しない動作やクラッシュを防ぐことができます。

競合状態の回避

競合状態は、複数のスレッドが同時に共有リソースにアクセスし、データの整合性が失われる状況を指します。

競合状態を回避するためには、以下の方法を用います。

  • ミューテックスの使用: 共有リソースへのアクセスをミューテックスで保護することで、同時アクセスを防ぎます。
  • クリティカルセクションの最小化: クリティカルセクション(ミューテックスで保護されたコードブロック)を最小限にすることで、スレッドの待ち時間を減らします。

以下は、競合状態を回避するためのミューテックスの使用例です。

#include <pthread.h>
#include <stdio.h>
int counter = 0;
pthread_mutex_t mutex;
void* incrementCounter(void* arg) {
    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&mutex);
        counter++;
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}
int main() {
    pthread_t thread1, thread2;
    pthread_mutex_init(&mutex, NULL);
    pthread_create(&thread1, NULL, incrementCounter, NULL);
    pthread_create(&thread2, NULL, incrementCounter, NULL);
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    printf("カウンターの最終値: %d\n", counter);
    pthread_mutex_destroy(&mutex);
    return 0;
}

デッドロックの防止

デッドロックは、複数のスレッドが互いにロックを待ち続ける状態で、プログラムが停止してしまう問題です。

デッドロックを防ぐためには、以下の方法を考慮します。

  • ロックの順序を統一: 複数のロックを取得する際には、常に同じ順序でロックを取得するようにします。
  • タイムアウトの設定: ロック取得にタイムアウトを設定し、一定時間内に取得できない場合は処理を中断します。

以下は、デッドロックを防ぐためのロック順序の統一例です。

#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mutex1;
pthread_mutex_t mutex2;
void* threadFunction1(void* arg) {
    pthread_mutex_lock(&mutex1);
    pthread_mutex_lock(&mutex2);
    printf("スレッド1が両方のロックを取得しました\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}
void* threadFunction2(void* arg) {
    pthread_mutex_lock(&mutex1);
    pthread_mutex_lock(&mutex2);
    printf("スレッド2が両方のロックを取得しました\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}
int main() {
    pthread_t thread1, thread2;
    pthread_mutex_init(&mutex1, NULL);
    pthread_mutex_init(&mutex2, NULL);
    pthread_create(&thread1, NULL, threadFunction1, NULL);
    pthread_create(&thread2, NULL, threadFunction2, NULL);
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    pthread_mutex_destroy(&mutex1);
    pthread_mutex_destroy(&mutex2);
    return 0;
}

スレッドセーフな関数の設計

スレッドセーフな関数とは、複数のスレッドから同時に呼び出されても正しく動作する関数です。

スレッドセーフな関数を設計するためには、以下の点に注意します。

  • 共有データの保護: 共有データにアクセスする際には、ミューテックスなどで保護します。
  • 再入可能性の確保: 関数が再入可能であることを確認し、グローバル変数や静的変数を使用しないようにします。
  • ローカル変数の使用: 関数内で使用する変数は、可能な限りローカル変数を使用します。

以下は、スレッドセーフな関数の設計例です。

#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mutex;
void incrementCounter(int* counter) {
    pthread_mutex_lock(&mutex);
    (*counter)++;
    pthread_mutex_unlock(&mutex);
}
void* threadFunction(void* arg) {
    int* counter = (int*)arg;
    for (int i = 0; i < 100000; i++) {
        incrementCounter(counter);
    }
    return NULL;
}
int main() {
    int counter = 0;
    pthread_t thread1, thread2;
    pthread_mutex_init(&mutex, NULL);
    pthread_create(&thread1, NULL, threadFunction, &counter);
    pthread_create(&thread2, NULL, threadFunction, &counter);
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    printf("カウンターの最終値: %d\n", counter);
    pthread_mutex_destroy(&mutex);
    return 0;
}

この例では、incrementCounter関数がスレッドセーフに設計されており、複数のスレッドから同時に呼び出されても正しく動作します。

応用例

pthreadを利用したスレッドプログラミングは、さまざまな応用が可能です。

ここでは、いくつかの応用例を紹介します。

マルチスレッドによる並列計算

マルチスレッドを利用することで、計算を並列化し、処理速度を向上させることができます。

以下は、配列の要素を並列に加算する例です。

#include <pthread.h>
#include <stdio.h>
#define NUM_THREADS 4
#define ARRAY_SIZE 1000
int array[ARRAY_SIZE];
int sum = 0;
pthread_mutex_t mutex;
void* sumArray(void* arg) {
    int start = *(int*)arg;
    int localSum = 0;
    for (int i = start; i < start + ARRAY_SIZE / NUM_THREADS; i++) {
        localSum += array[i];
    }
    pthread_mutex_lock(&mutex);
    sum += localSum;
    pthread_mutex_unlock(&mutex);
    return NULL;
}
int main() {
    pthread_t threads[NUM_THREADS];
    int indices[NUM_THREADS];
    pthread_mutex_init(&mutex, NULL);
    // 配列の初期化
    for (int i = 0; i < ARRAY_SIZE; i++) {
        array[i] = 1; // 例としてすべての要素を1に設定
    }
    // スレッドの作成
    for (int i = 0; i < NUM_THREADS; i++) {
        indices[i] = i * (ARRAY_SIZE / NUM_THREADS);
        pthread_create(&threads[i], NULL, sumArray, &indices[i]);
    }
    // スレッドの終了を待機
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }
    printf("配列の合計: %d\n", sum);
    pthread_mutex_destroy(&mutex);
    return 0;
}

この例では、配列を4つのスレッドで分割して並列に加算し、最終的な合計を計算しています。

スレッドを用いたサーバーの実装

スレッドを用いることで、サーバーは複数のクライアントからのリクエストを同時に処理することができます。

以下は、簡単なスレッドベースのサーバーの例です。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
void* handleClient(void* arg) {
    int clientSocket = *(int*)arg;
    char buffer[BUFFER_SIZE];
    int bytesRead;
    // クライアントからのデータを受信
    while ((bytesRead = read(clientSocket, buffer, BUFFER_SIZE)) > 0) {
        // 受信したデータをそのままクライアントに送信
        write(clientSocket, buffer, bytesRead);
    }
    close(clientSocket);
    return NULL;
}
int main() {
    int serverSocket, clientSocket;
    struct sockaddr_in serverAddr, clientAddr;
    socklen_t addrLen = sizeof(clientAddr);
    pthread_t thread;
    // ソケットの作成
    serverSocket = socket(AF_INET, SOCK_STREAM, 0);
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = INADDR_ANY;
    serverAddr.sin_port = htons(PORT);
    // ソケットのバインド
    bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
    // 接続の待機
    listen(serverSocket, 5);
    printf("サーバーが起動しました。ポート: %d\n", PORT);
    while ((clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &addrLen)) >= 0) {
        // クライアントごとにスレッドを作成
        pthread_create(&thread, NULL, handleClient, &clientSocket);
        pthread_detach(thread); // スレッドを分離状態に設定
    }
    close(serverSocket);
    return 0;
}

この例では、サーバーがクライアントからの接続を受け入れるたびに新しいスレッドを作成し、クライアントのリクエストを処理します。

スレッドプールの構築

スレッドプールは、あらかじめ一定数のスレッドを作成しておき、タスクを効率的に処理するための手法です。

以下は、簡単なスレッドプールの例です。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM_THREADS 4
#define NUM_TASKS 10
typedef struct {
    void (*function)(void*);
    void* arg;
} Task;
Task taskQueue[NUM_TASKS];
int taskCount = 0;
pthread_mutex_t mutex;
pthread_cond_t cond;
void* threadFunction(void* arg) {
    while (1) {
        Task task;
        pthread_mutex_lock(&mutex);
        while (taskCount == 0) {
            pthread_cond_wait(&cond, &mutex);
        }
        task = taskQueue[--taskCount];
        pthread_mutex_unlock(&mutex);
        task.function(task.arg);
    }
    return NULL;
}
void addTask(void (*function)(void*), void* arg) {
    pthread_mutex_lock(&mutex);
    taskQueue[taskCount++] = (Task){function, arg};
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
}
void exampleTask(void* arg) {
    int taskId = *(int*)arg;
    printf("タスク %d を実行中\n", taskId);
}
int main() {
    pthread_t threads[NUM_THREADS];
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_create(&threads[i], NULL, threadFunction, NULL);
    }
    for (int i = 0; i < NUM_TASKS; i++) {
        int* taskId = malloc(sizeof(int));
        *taskId = i;
        addTask(exampleTask, taskId);
    }
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

この例では、スレッドプールを使用してタスクを効率的に処理しています。

タスクはキューに追加され、スレッドが順次処理を行います。

よくある質問

pthreadを使う際の注意点は?

pthreadを使用する際には、以下の点に注意が必要です。

  • リソースの管理: スレッドを作成するたびにリソースが消費されるため、必要以上にスレッドを作成しないようにします。

スレッドプールを利用することで、リソースの効率的な管理が可能です。

  • 同期の適切な使用: 競合状態を防ぐために、ミューテックスや条件変数を適切に使用します。

同期が不十分だと、データの不整合が発生する可能性があります。

  • デッドロックの回避: 複数のロックを取得する際には、常に同じ順序で取得するようにし、デッドロックを防ぎます。

スレッドのデバッグ方法は?

スレッドのデバッグは、通常のプログラムよりも難しい場合がありますが、以下の方法でデバッグを行うことができます。

  • ログ出力: スレッドの開始、終了、重要な操作の前後にログを出力することで、スレッドの動作を追跡します。

例:printf("スレッドが開始されました\n");

  • デバッガの使用: GDBなどのデバッガを使用して、スレッドの状態を確認します。

デバッガはスレッドごとにブレークポイントを設定できるため、特定のスレッドの動作を詳細に調査できます。

  • ツールの活用: ValgrindやHelgrindなどのツールを使用して、競合状態やデッドロックを検出します。

スレッド数の最適化はどうすれば良い?

スレッド数の最適化は、プログラムのパフォーマンスを向上させるために重要です。

以下の方法を考慮します。

  • ハードウェアの特性を考慮: CPUのコア数に基づいてスレッド数を設定します。

一般的に、スレッド数はコア数と同じか、それより少し多い程度が適切です。

  • タスクの性質を考慮: I/O待ちが多いタスクの場合、スレッド数を増やすことで待ち時間を隠蔽できます。

一方、CPUバウンドなタスクでは、スレッド数をコア数に合わせることが望ましいです。

  • プロファイリング: プロファイリングツールを使用して、スレッド数を変化させた際のパフォーマンスを測定し、最適なスレッド数を決定します。

まとめ

この記事では、C言語におけるpthreadの基本的な使い方から、スレッドの同期や安全性、応用例までを詳しく解説しました。

pthreadを活用することで、マルチスレッドプログラミングの可能性を広げ、効率的なプログラム設計が可能になります。

これを機に、実際のプロジェクトでpthreadを活用し、プログラムのパフォーマンス向上に挑戦してみてはいかがでしょうか。

当サイトはリンクフリーです。出典元を明記していただければ、ご自由に引用していただいて構いません。

関連カテゴリーから探す

  • URLをコピーしました!
目次から探す