C言語のfork関数を使って並列処理を行う方法を解説

C言語では、fork関数を使って並列処理を行うことができます。

この記事では、fork関数の基本的な使い方から、実際にサンプルコードを用いて並列処理を行う方法まで、初心者にもわかりやすく解説します。

目次

fork関数とは

fork関数は、Unix系のオペレーティングシステムでプロセスを複製するために使用される関数です。

Unix系限定であるためWindowsでは使用できません。

親プロセスから呼び出されると、新しい子プロセスが作成されます。

子プロセスは、親プロセスのメモリ空間をコピーして独立したプロセスとして実行されます。

fork関数の使い方

fork関数は、unistd.hヘッダファイルに定義されています。以下は、fork関数を使用して子プロセスを作成する簡単な例です。

#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid;

    pid = fork();

    if (pid == 0) {
        printf("I am the child process\n");
    } else if (pid > 0) {
        printf("I am the parent process\n");
    } else {
        printf("Fork failed\n");
        return 1;
    }

    return 0;
}

この例では、親プロセスがfork関数を呼び出し、新しい子プロセスが作成されます。子プロセスは「I am the child process」というメッセージを表示し、親プロセスは「I am the parent process」というメッセージを表示します。

fork関数による並列処理

fork関数を使用することで、複数のタスクを同時に実行することができます。以下は、2つのタスク(AとB)を並列に実行する例です。

#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid;

    pid = fork();

    if (pid == 0) {
        // タスクAの処理
        printf("Task A is running\n");
    } else if (pid > 0) {
        // タスクBの処理
        printf("Task B is running\n");
    } else {
        printf("Fork failed\n");
        return 1;
    }

    return 0;
}

この例では、親プロセスがタスクBを実行し、子プロセスがタスクAを実行します。それぞれのタスクは独立して実行されるため、並列処理が可能になります。

fork関数を使った並列処理の基本

C言語には、fork関数というプロセスを複製するための関数があります。このfork関数を使うことで、親プロセスから子プロセスを作成し、それぞれ別々の処理を同時に実行することができます。

fork関数の戻り値

fork関数は、親プロセスでは新しい子プロセスのPID(プロセスID)を返し、子プロセスでは0を返します。エラーが発生した場合は-1を返します。

#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid;

    pid = fork();

    if (pid == -1) {
        printf("Error: fork failed\n");
        return 1;
    } else if (pid == 0) {
        printf("I am child process, my PID is %d\n", getpid());
    } else {
        printf("I am parent process, my PID is %d and my child's PID is %d\n", getpid(), pid);
    }

    return 0;
}

上記のコードでは、まずfork関数で新しい子プロセスを作成しています。その後、親プロセスか子プロセスかによって出力するメッセージが異なるように分岐しています。

親プロセスと子プロセスの処理の分岐

親プロセスと子プロセスはそれぞれ独立した処理を行うため、どちらか一方だけが実行されるように制御する必要があります。これはif文やswitch文などでPID(process ID)による条件分岐を行うことで実現できます。以下は例です。

#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid;

    pid = fork();

    if (pid == -1) {
        printf("Error: fork failed\n");
        return 1;
    } else if (pid == 0) { // 子プロセス
        printf("I am child process, my PID is %d\n", getpid());
        
        // 子プロセス用の処理
        
        return 0; // 子プロセス終了
    } else { // 親プロセス
        printf("I am parent process, my PID is %d and my child's PID is %d\n", getpid(), pid);
        
        // 親プロセス用の処理
        
        wait(NULL); // 子プロセス終了待ち
        
        return 0; // 親プロセス終了
    }
}

上記のコードでは、親・子両方でそれぞれ異なる処理を行っています。また、wait(NULL)によって親プロセスが子プロセスの終了を待つようにしています。

fork関数を使った並列処理の応用

続いては、fork関数を使ったより実践的な並列処理の方法について解説します。

複数の子プロセスを生成する方法

fork関数を使うことで、親プロセスから1つの子プロセスが生成されますが、複数の子プロセスを生成することも可能です。具体的には、以下のようにforループなどで複数回fork関数を呼び出すことで実現できます。

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

int main() {
    int i;
    for (i = 0; i < 3; i++) {
        pid_t pid = fork();
        if (pid == -1) {
            perror("fork");
            exit(EXIT_FAILURE);
        } else if (pid == 0) {
            printf("I'm child process %d\n", i);
            exit(EXIT_SUCCESS);
        }
    }
    printf("I'm parent process\n");
    return 0;
}

上記のコードでは、forループ内で3回fork関数が呼び出されています。各ループごとに子プロセスが生成され、それぞれ異なるメッセージが表示されます。

子プロセスの終了を待つ方法

複数の子プロセスを生成した場合、親プロセスは各子プロセスが正常に終了するまで待機する必要があります。

これはwaitpid関数を使用して実現できます。waitpid関数は指定したPID(Process ID)の子プロセスが終了するまで待機し、その終了状態(正常終了か異常終了か)や終了コード(exit関数で指定した値)などを取得することができます。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    int i;
    for (i = 0; i < 3; i++) {
        pid_t pid = fork();
        if (pid == -1) {
            perror("fork");
            exit(EXIT_FAILURE);
        } else if (pid == 0) {
            printf("I'm child process %d\n", i);
            exit(i); // 異なる値で終了させる
        }
    }

    int status;
    pid_t w_pid;
    while ((w_pid = waitpid(-1, &status, WUNTRACED | WCONTINUED)) != -1) { // 全ての子プロセスが終了するまで繰り返す
        if (WIFEXITED(status)) { // 正常に終了した場合
            printf("child process %d exited with status %d\n", w_pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) { // シグナルによって強制終了した場合
            printf("child process %d killed by signal %d\n", w_pid, WTERMSIG(status));
        } else if (WIFSTOPPED(status)) { // 停止した場合
            printf("child process %d stopped by signal %d\n", w_pid, WSTOPSIG(status));
        } else if (WIFCONTINUED(status)) { // 再開した場合
            printf("child process %d resumed\n", w_pid);
        }
        
        if (w_pid == -1) { // 全ての子プロセスが完全に終了したらループから抜ける
            break;
        }
    }

    printf("All child processes have finished.\n");
    
    return 0;
}

上記のコードでは、waitpid関数内部で全ての子プロセスが完全に終了するまで繰り返し処理しています。

また、waitpid関数から戻ってきたstatus変数から各種情報(正常/異常/停止/再開)を取得して表示しています。

子プロセスの終了ステータスを取得する方法

先程紹介したwaitpid関数では、各子プロセスから返されたexitコード(main関数内部やexit関数内部で指定された値)も取得可能です。具体的にはWEXITSTATUSマクロを使用します。

if (WIFEXITED(status)) { // 正常に終了した場合
   printf("child process %d exited with status %d\n", w_pid, WEXITSTATUS(status));
}

上記コードでは、「正常に終了」かつ「exitコードあり」だった場合に限り、exitコードも表示しています。

fork関数を使った並列処理の注意点

fork関数を使った並列処理には、以下のような注意点があります。

プロセスの生成数による負荷の増加

fork関数を使って子プロセスを生成すると、親プロセスと同じプログラムが複製されます。

そのため、子プロセスが多くなるほどメモリやCPUの使用量が増え、システム全体の負荷が上昇します。そのため、必要以上に多くの子プロセスを生成しないように注意する必要があります。

プロセス間のデータの共有

fork関数で生成された子プロセスは、親プロセスとは別々のメモリ空間を持ちます。

そのため、親プロセスで定義した変数や配列などは、子プロセスから直接参照することができません。この問題を解決する方法としては、IPC(Inter-Process Communication)機能を利用する方法や、ファイルやパイプなどを介してデータを共有する方法があります。

以上がfork関数を使った並列処理における注意点です。これらに気をつけて適切に使用すれば、効率的な並列処理が実現できます。

目次