C言語とスレッド処理の完全ガイド!初心者でも理解できる10のステップ

C言語とスレッド処理の学習に最適な記事のサムネイルC言語
この記事は約19分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

プログラミングの世界には無限の可能性があります。

特に「C言語」と「スレッド処理」はその中でも特に重要な要素と言えるでしょう。

本記事では、これら二つの要素について初心者でも理解できるように詳しく説明していきます。

●C言語とは

C言語は、1972年にAT&Tベル研究所で開発されたプログラミング言語です。

その性能の高さと汎用性のある特性から、現在でも広く使われています。

○C言語の特徴

C言語の最大の特徴はその汎用性と効率性です。

C言語はハードウェアに近いレベルで動作するため、システム開発や組み込みシステム開発などによく使用されます。

また、C言語で書かれたコードは他の多くのプログラミング言語に移植することが可能です。

●スレッド処理とは

スレッド処理とは、プログラム内で複数の処理を同時に行うための技術のことを指します。

この技術により、複数の処理を並列に行うことで、全体の処理速度を向上させることができます。

○スレッド処理の基本

スレッド処理は、プログラム内で行う処理を分割し、それぞれを独立したスレッドとして同時に実行することで処理速度を向上させます。

各スレッドは独立して動作するため、ひとつのスレッドがブロックされても他のスレッドはそのまま進行します。

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

スレッドとプロセスは、同じプログラム内で独立して動作する単位としてよく比較されます。

ただし、プロセスはOSによって管理され、メモリ空間も独立しています。一方、スレッドはプロセス内で生成され、メモリ空間を共有します。

●C言語でのスレッド処理の基本

C言語でスレッド処理を実行するには、pthreadというライブラリを使用します。

○pthreadライブラリの紹介

pthreadライブラリは、POSIX規格で定められたスレッド処理を実装するためのライブラリです。

C言語を含む多くのプログラミング言語で利用することができます。

○スレッドの生成方法

スレッドの生成は、pthread_create関数を使用します。

この関数を使ってスレッドを生成すると、新たに作られたスレッドは指定した関数から実行を開始します。

□サンプルコード1:スレッドの生成

下記のコードでは、pthread_create関数を使って新しいスレッドを生成し、そのスレッドでhello関数を実行しています。

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

void *hello(void *arg) {
    printf("Hello, world!\n");
    return NULL;
}

int main(void) {
    pthread_t thread_id;
    pthread_create(&thread_id, NULL, hello, NULL);
    pthread_join(thread_id, NULL);
    return 0;
}

このコードを実行すると、新しく生成されたスレッドで”Hello, world!”という文字列を出力します。

●スレッドの終了と結合

スレッドの終了と結合も重要な要素です。

スレッドが終了したとき、その結果を主スレッドが取得できるようにすることが重要です。

○スレッドの終了方法

スレッドは、次のいずれかの条件を満たすと終了します。

  1. スレッドが最初に呼び出した関数からリターンしたとき
  2. スレッドが他のスレッドからpthread_cancel関数を呼び出されたとき
  3. 全スレッドが終了し、プログラムが終了したとき

それでは、スレッドを終了する具体的な方法について見てみましょう。

□サンプルコード2:スレッドの終了

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

void *thread_func(void *arg){
    printf("スレッドが終了します。\n");
    pthread_exit(NULL);
}

int main(){
    pthread_t thread;
    pthread_create(&thread, NULL, thread_func, NULL);
    pthread_join(thread, NULL);
    return 0;
}

このコードでは、pthread_exit関数を使ってスレッドを明示的に終了しています。

この例では、thread_func関数がスレッドの実行内容として定義され、その中でpthread_exit関数が呼び出されています。その結果、スレッドが終了します。

このコードを実行すると、次のような出力が得られます。

スレッドが終了します。

この出力は、スレッドがpthread_exit関数に到達し、終了したことを表しています。

○スレッドの結合方法

スレッドが終了したら、そのスレッドのリソースを解放するために、pthread_join関数を用いてメインスレッドから結合します。

スレッドの結合は、スレッドが終了するまでメインスレッドをブロック(一時停止)します。

そのため、pthread_join関数はスレッドが終了するまで待機する役割も果たします。

□サンプルコード3:スレッドの結合

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

void *thread_func(void *arg){
    printf("スレッドが終了します。\n");
    pthread_exit(NULL);
}

int main(){
    pthread_t thread;
    pthread_create(&thread, NULL, thread_func, NULL);
    pthread_join(thread, NULL);
    printf("スレッドを結合しました。\n");
    return 0;
}

このコードでは、pthread_join関数を使ってスレッドを結合しています。

この例では、thread_func関数がスレッドの実行内容として定義され、その中でpthread_exit関数が呼び出されています。

その後、pthread_join関数でスレッドが結合され、”スレッドを結合しました。”と表示されます。

このコードを実行すると、次のような出力が得られます。

スレッドが終了します。
スレッドを結合しました。

この出力は、スレッドが終了し、その後でスレッドが正常に結合されたことを表しています。

このようにスレッドの結合は、スレッドの終了を確認し、そのリソースを適切に解放するために重要です。

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

スレッド間のデータ共有は、スレッドの真価を引き出すための重要な要素です。

複数のスレッドが同時に動作する場合、それぞれのスレッドが異なるデータに対して操作を行うと、データの不整合が発生する可能性があります。

これを避けるために、スレッド間でデータを共有し、同期を取ることが必要です。

○共有メモリの利用方法

共有メモリは、複数のスレッドが同じメモリ領域にアクセスできるようにする手法です。

これにより、異なるスレッド間で情報をやり取りすることが可能になります。

しかし、共有メモリを利用する際には、スレッドが同時に同じメモリにアクセスしないように管理する必要があります。

これを管理しないと、データの不整合や競合が発生する可能性があります。

共有メモリを利用したC言語でのスレッド処理の例を紹介します。

□サンプルコード4:共有メモリの利用

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

int count = 0; // 共有メモリ(グローバル変数)

void* thread_function(void* arg) {
    for(int i = 0; i < 1000000; i++) {
        count++; // 共有メモリの更新
    }
    return NULL;
}

int main() {
    pthread_t thread1, thread2;

    pthread_create(&thread1, NULL, thread_function, NULL);
    pthread_create(&thread2, NULL, thread_function, NULL);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    printf("Count: %d\n", count);

    return 0;
}

このコードでは、グローバル変数を使ってスレッド間のデータ共有を実現しています。

スレッドを2つ生成し、それぞれが同じグローバル変数countを1000000回インクリメントします。

すべてのスレッドが終了した後で、countの値を出力します。

しかし、このコードを実行すると、期待した結果(2000000)とは異なる結果が出力されることがあります。

これは、複数のスレッドが同時にcountを更新しようとした結果、更新が失われる(競合状態)ためです。

この問題を解決するためには、スレッド間の同期が必要です。

○スレッド間の同期方法

スレッド間の同期を取るためには、mutex(ミューテックス)やsemaphore(セマフォ)などの同期メカニズムを利用します。

これらのメカニズムを使用すると、一度に一つのスレッドだけが特定のコードセクション(クリティカルセクション)を実行できるようになります。

ミューテックスを使用したスレッド間の同期のサンプルコードを紹介します。

□サンプルコード5:スレッド間の同期

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

int count = 0; // 共有メモリ(グローバル変数)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // ミューテックスの初期化

void* thread_function(void* arg) {
    for(int i = 0; i < 1000000; i++) {
        pthread_mutex_lock(&mutex); // ミューテックスのロック
        count++; // 共有メモリの更新
        pthread_mutex_unlock(&mutex); // ミューテックスのアンロック
    }
    return NULL;
}

int main() {
    pthread_t thread1, thread2;

    pthread_create(&thread1, NULL, thread_function, NULL);
    pthread_create(&thread2, NULL, thread_function, NULL);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    printf("Count: %d\n", count);

    return 0;
}

このコードでは、ミューテックスを使用して、共有メモリ(count)へのアクセスを同期化しています。

各スレッドがcountをインクリメントする前にミューテックスをロックし、インクリメント後にアンロックします。

これにより、一度に一つのスレッドだけがcountを更新できるようになり、競合状態を避けることができます。

このコードを実行すると、期待通りの結果(2000000)が出力されます。

●スレッドセーフとは

スレッドセーフとは、複数のスレッドから同時に呼び出された場合でも、適切に動作する関数やライブラリのことを指します。

スレッドセーフなコードは、並行処理やマルチスレッド環境での安全な動作を保証します。

○スレッドセーフの定義と注意点

スレッドセーフとは、複数のスレッドが同時にアクセスしても、結果の整合性が保たれるようなコードの性質を指します。

しかし、全ての関数やライブラリがスレッドセーフとは限りません。非スレッドセーフな関数やライブラリを複数のスレッドから同時に呼び出すと、データ競合や予期せぬエラーが発生する可能性があります。

そのため、スレッドセーフな関数やライブラリを使うか、スレッドセーフでない場合は排他制御などの同期手段を使用して、複数のスレッドが同時にアクセスしないようにする必要があります。

●C言語でのスレッド処理の応用例

C言語のスレッド処理は、さまざまな応用例があります。

それでは、同時に複数のタスクを実行する方法と、大量のデータを効率的に処理する方法について解説します。

○同時に複数のタスクを実行する

複数のスレッドを使うことで、タスクを並列に実行し、プログラムの実行時間を短縮することが可能です。

このような並列処理は、CPUのコア数をフルに活用して処理を高速化する効果があります。

ただし、同時にアクセスするデータには注意が必要で、データの不整合を防ぐために適切な同期処理が必要です。

□サンプルコード6:複数タスクの同時実行

次に、具体的なコードを見ていきましょう。

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

void *print_message(void *ptr) {
    char *message;
    message = (char *) ptr;
    printf("%s \n", message);
}

int main() {
    pthread_t thread1, thread2;
    char *message1 = "Thread 1";
    char *message2 = "Thread 2";

    pthread_create( &thread1, NULL, print_message, (void*) message1);
    pthread_create( &thread2, NULL, print_message, (void*) message2);

    pthread_join( thread1, NULL);
    pthread_join( thread2, NULL); 

    return 0;
}

このコードでは、二つのスレッドを作成し、それぞれ別々のメッセージを出力しています。

これは一つのプログラム内で複数のタスクを並行して実行する一例です。

○大量のデータを効率的に処理する

スレッド処理は、大量のデータを分割して並列に処理することで、処理時間を短縮することが可能です。

これは特に、CPUの計算能力を必要とする高負荷な処理や、大規模なデータセットを扱う際に有効です。

□サンプルコード7:大量データの効率処理

具体的なコードを見てみましょう。

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

#define SIZE                10000
#define NUMBER_OF_THREADS   3

void *sorter(void *params);  
void *merger(void *params);  

int list[SIZE];              
int result[SIZE];            

typedef struct {
    int from_index;
    int to_index;
} parameters;

int main (int argc, const char * argv[]) {
    srand(time(NULL));
    for(int i = 0; i < SIZE; i ++)
        list[i] = rand();

    pthread_t thread[NUMBER_OF_THREADS];

    parameters *data = (parameters *) malloc (sizeof(parameters));
    data->from_index = 0;
    data->to_index = (SIZE/2) - 1;
    pthread_create(&thread[0], 0, sorter, data);

    data = (parameters *) malloc (sizeof(parameters));
    data->from_index = (SIZE/2);
    data->to_index = SIZE - 1;
    pthread_create(&thread[1], 0, sorter, data);

    for(int i = 0; i < NUMBER_OF_THREADS-1; i++)
        pthread_join(thread[i], NULL);

    data = (parameters *) malloc(sizeof(parameters));
    data->from_index = 0;
    data->to_index = (SIZE/2);
    pthread_create(&thread[2], 0, merger, data);

    pthread_join(thread[2], NULL);

    printf("Sorted array:\n");
    for(int i = 0; i < SIZE; i++)
        printf("%d ", result[i]);

    return 0;
}

void *sorter(void *params) {
    parameters* p = (parameters *)params;
    int begin = p->from_index;
    int end = p->to_index;

    for(int i=begin; i<=end; i++) {
        for(int j=i+1; j<=end; j++) {
            if(list[i]>list[j]) {
                int temp=list[i];
                list[i]=list[j];
                list[j]=temp;
            }
        }
    }
    pthread_exit(NULL);
}

void *merger(void *params) {
    parameters* p = (parameters *)params;
    int resultCurrent=0;
    int begin = p->from_index;
    int end = p->to_index;

    for(int i=begin, j=end; i<=end-1, j<=SIZE-1;) {
        if(list[i]<=list[j]) {
            result[resultCurrent++]=list[i++];
        } else {
            result[resultCurrent++]=list[j++];
        }
    }
    pthread_exit(NULL);
}

このコードでは、配列にランダムな数値を入れ、それを二つのスレッドでソート(並び替え)を行い、最後に結果をマージ(結合)するスレッドを作成します。

これにより、大量のデータを効率的にソートすることができます。

●スレッド処理のエラー対処法

多重処理において、スレッド処理が必要なことは多々あります。

しかし、時折、エラーが発生することもあります。

今回は、一般的なエラーとそれらの対処法を解説します。

○エラーの例とその対処法

よくあるエラーとして、「スレッドの生成に失敗した」というものがあります。

これは、スレッドを生成する際に使用する関数がエラーを返す場合に発生します。

一般的には、これはシステムのリソースが不足していることを表します。

このような場合、不要なプロセスを終了させたり、システムのリソースを増やすことで対処できます。

また、「スレッドが予期せず終了する」というエラーも頻繁に発生します。

これは通常、プログラム内のバグが原因となります。例えば、メモリアクセスの違反や除算エラーなどが考えられます。

このようなエラーの対処法としては、コードを見直し、デバッグを行うことが有効です。

最後に、「スレッド間でデータが正しく共有されない」というエラーもあります。

これは、スレッド間でのデータの同期がうまく行っていない場合に発生します。

これを解決するには、正しい同期メカニズムを使用して、データの競合を避ける必要があります。

これらは一部のエラー例ですが、実際にはこれら以外にも様々なエラーが発生する可能性があります。

それぞれのエラーが発生した時には、具体的なエラーメッセージや状況をもとに、適切な対処法を選択することが重要です。

●スレッド処理のカスタマイズ方法

C言語でのスレッド処理では、様々なカスタマイズが可能です。

例えば、スレッドの優先度を設定することができます。

これにより、処理の重要性に応じてスレッドの実行順序を調整することが可能になります。

○スレッドの優先度設定

スレッドの優先度は、pthreadライブラリの関数を使って設定することができます。

具体的には、pthread_attr_t構造体とpthread_setschedparam関数を使用します。

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

void *threadFunc(void *arg) {
    // ここでスレッドの処理を記述
    printf("Hello from thread\\n");
    return NULL;
}

int main() {
    pthread_t thread;
    pthread_attr_t attr;
    struct sched_param param;

    pthread_attr_init(&attr);
    param.sched_priority = 20; // 優先度を20に設定
    pthread_attr_setschedparam(&attr, &param);
    pthread_create(&thread, &attr, threadFunc, NULL);

    pthread_join(thread, NULL);
    return 0;
}

このコードでは、スレッドの優先度を20に設定しています。

この値が大きいほど、スレッドの優先度が高くなります。

この例では、threadFunc関数を実行する新しいスレッドを生成し、そのスレッドの優先度を20に設定しています。

なお、優先度の範囲はシステムにより異なります。

範囲を超えた値を設定した場合、pthread_setschedparam関数はエラーを返します。

そのため、優先度を設定する前に、sched_get_priority_min関数とsched_get_priority_max関数を使用して、優先度の範囲を確認することをお勧めします。

以上、C言語とスレッド処理の基本から応用、そしてエラー対処法やカスタマイズ方法について解説してきました。

スレッド処理は複雑な面もありますが、理解して使いこなすことで、プログラムの効率を大幅に向上させることが可能です。

まとめ

C言語でのスレッド処理は、プログラムを効率的に実行するための強力な手段です。

しかし、その強力さゆえに、扱いが難しく、エラーも起こりやすいです。

今回はそのようなエラーの対処法や、より効果的なスレッド処理のためのカスタマイズ方法についても解説しました。

これからも、プログラミングの世界で成長し続けるために、新たな知識を学んでいきましょう。

今後もこのようなガイドを通じて、皆さんの学習が進むことを願っています。