第1章: 並行プログラミングの理論と食事する哲学者問題

1.1 並行計算の歴史

タイムシェアリングシステムの誕生

1961年、MITのFernando CorbatóらはCTSS(Compatible Time-Sharing System)を開発した。これは、複数のユーザーが1台のコンピュータを「同時に」利用できる最初のタイムシェアリングシステムであった。

Corbatóの洞察:

コンピュータは人間より遥かに速い
→ 人間がキーを打つ間にCPUは何千もの命令を実行できる
→ 複数のユーザーを高速に切り替えれば、全員が専用機を使っているように感じる

この概念は、並行計算(Concurrent Computing)の基礎となった。Corbatóは後にこの業績でチューリング賞を受賞した(1990年)。

Edsger Dijkstraと並行プログラミング

1960年代、オランダのコンピュータ科学者Edsger Dijkstraは、並行プログラミングの理論的基盤を確立した。Dijkstraの貢献は計り知れない:

  • セマフォの発明(1965年): 同期プリミティブの提案
  • 食事する哲学者問題(1965年): リソース競合の教育的モデル
  • 銀行家のアルゴリズム(1965年): デッドロック回避アルゴリズム
  • 構造化プログラミング(1968年): goto文の害悪

Dijkstraは"Cooperating Sequential Processes"(1965年)において、並行プログラミングの本質的な困難さを初めて形式的に記述した。

Carl Adam Petriとペトリネット

1962年、ドイツの数学者Carl Adam Petriは博士論文においてペトリネット(Petri Net)を発明した。これは並行システムを数学的にモデル化する形式手法である。

ペトリネットの構成要素:

  • プレース(Place): 状態を表す円
  • トランジション(Transition): 遷移を表す四角形
  • トークン(Token): 現在の状態を示すマーカー

哲学者問題のペトリネット表現:

  (思考中)         (フォーク利用可能)
     ○──────→ □ ←───────○
      P1        T1         F1
               │
               ↓
          (食事中)
             ○
             P2

ペトリネットは、デッドロックの検出や到達可能性分析に利用される。

1.2 食事する哲学者問題

Dijkstraの原典(1965年)

Dijkstraは"Cooperating Sequential Processes"(1965年)において、以下の問題を提示した:

> N人の哲学者が円形のテーブルに座っている。各哲学者の前にはスパゲッティの皿があり、隣り合う哲学者の間に1本ずつフォークが置かれている。哲学者は食事をするために、両側の2本のフォークを必要とする。

この問題は、コンピュータシステムにおける資源競合を象徴的に表現している。

問題の形式的定義

状態空間:

  • 各哲学者は状態$S_i \in \{THINKING, HUNGRY, EATING, SLEEPING\}$を持つ
  • 各フォークは状態$F_j \in \{FREE, TAKEN\}$を持つ

不変条件:

  • 隣り合う哲学者は同時に食事できない
  • 食事をするには両側のフォークが必要

安全性要件:

  • デッドロック自由: システムは常に進行可能
  • 餓死自由: 全ての哲学者はいつか食事できる
  • 相互排除: 共有リソースへの安全なアクセス
  • デッドロックの条件(Coffman条件)

    1971年、E.G. Coffman, M.J. Elphick, A. Shoshaniは論文"System Deadlocks"において、デッドロック発生の必要十分条件を定式化した("Coffman条件"):

  • 相互排除(Mutual Exclusion): リソースは一度に1つのプロセスのみが使用
  • 保持と待機(Hold and Wait): リソースを保持しながら他のリソースを待機
  • 非横取り(No Preemption): リソースは自発的にのみ解放される
  • 循環待機(Circular Wait): プロセスの循環的な待機チェーンが存在

食事する哲学者問題において:

  • 相互排除: フォークは1人の哲学者のみ使用可能 ✓
  • 保持と待機: 1本のフォークを持ちながらもう1本を待つ ✓
  • 非横取り: フォークは所有者のみ解放可能 ✓
  • 循環待機: 全員が左のフォークを持ち、右を待つと発生 ✓

4条件のうち1つでも破れば、デッドロックは発生しない。

1.3 プロセスとスレッドの理論

プロセスの概念史

プロセス(Process)という用語は、1960年代のMulticsプロジェクトで確立された。

Multicsの設計者たちは、実行中のプログラムを表す概念が必要だと認識した。プロセスは以下を含む:

  • プログラムコード
  • データ(ヒープ、スタック)
  • 実行状態(レジスタ、PC)
  • リソース(ファイル、メモリ)

プロセスの形式的定義(Silberschatz, "Operating System Concepts"):

プロセス = 実行中のプログラムのインスタンス
         = (コード, データ, 実行コンテキスト, リソース)

スレッドの概念史

スレッド(Thread)という概念は、1960年代後半から1970年代にかけて発展した。

用語の由来: "thread of execution"(実行の糸)から。プログラムの実行を「糸」に例え、複数の糸が同時に進行するイメージ。

スレッドの定義(IEEE POSIX.1c-1995):

スレッド = プロセス内の実行の流れ
         = (スタック, レジスタ, スケジューリング属性)
         ※アドレス空間はプロセス内で共有

プロセス vs スレッドの比較

| 属性 | プロセス | スレッド | |-----|---------|---------| | アドレス空間 | 独立 | 共有 | | 作成コスト | 高い | 低い | | コンテキストスイッチ | 遅い | 速い | | 通信 | IPC必要 | メモリ共有 | | 障害の影響 | 独立 | 全スレッドに波及 | | 同期 | 複雑 | ミューテックス等 |

なぜ食事する哲学者問題はスレッドで実装するのか

  • メモリ共有: フォーク状態を全哲学者が共有
  • 軽量性: プロセスより生成・切り替えコストが低い
  • 同期容易: ミューテックスで直接メモリを保護可能
  • 実践的: 実際のサーバーアプリケーションと同様の構造

1.4 並行性と並列性

Rob Pikeの定義

Go言語の設計者Rob Pikeは、並行性と並列性の違いを明確に定義した("Concurrency is not Parallelism", 2012):

並行性(Concurrency): > "Concurrency is about dealing with lots of things at once." > (並行性は、複数の物事を同時に扱うこと)

時間 →
CPU:  [A][B][A][C][B][A][C][B]
       タイムスライシングによる切り替え

並列性(Parallelism): > "Parallelism is about doing lots of things at once." > (並列性は、複数の物事を同時に実行すること)

時間 →
CPU1: [A][A][A][A][A][A][A][A]
CPU2: [B][B][B][B][B][B][B][B]
       物理的に同時実行

Amdahlの法則(1967年)

Gene Amdahlは1967年に、並列化による高速化の限界を定式化した("Validity of the single processor approach to achieving large scale computing capabilities"):

$$S(n) = \frac{1}{(1-p) + \frac{p}{n}}$$

ここで:

  • $S(n)$: n個のプロセッサでの高速化率
  • $p$: 並列化可能な部分の割合
  • $n$: プロセッサ数

例: 90%が並列化可能な場合($p = 0.9$)

  • $n = 2$: $S = 1.82$倍
  • $n = 10$: $S = 5.26$倍
  • $n = \infty$: $S = 10$倍(限界)

並列化不可能な10%が、全体の性能を制限する。

Gustafsonの法則(1988年)

John Gustafsonは、Amdahlの法則に対する補完的な見方を提案した:

$$S(n) = n - (1-p)(n-1)$$

問題サイズを大きくすれば、並列化可能部分も増大する。つまり、十分に大きな問題では、Amdahlの限界を超えられる。

1.5 42のPhilosophersプロジェクト

プロジェクト仕様

42のPhilosophersプロジェクトでは、Dijkstraの問題を以下の形式で実装する:

./philo number_of_philosophers time_to_die time_to_eat time_to_sleep \
        [number_of_times_each_philosopher_must_eat]

引数:

  • number_of_philosophers: 哲学者の人数(= フォーク数)
  • time_to_die: 最後の食事開始から死亡までの時間(ms)
  • time_to_eat: 食事の所要時間(ms)
  • time_to_sleep: 睡眠の所要時間(ms)
  • number_of_times_each_philosopher_must_eat: 終了条件(オプション)
  • 状態遷移モデル

    各哲学者の状態遷移を有限オートマトンで表現:

               取得成功
             ┌─────────┐
             ↓         │
    [THINKING] ─→ [HUNGRY] ─→ [EATING] ─→ [SLEEPING]
         ↑                                      │
         └──────────────────────────────────────┘
                     睡眠完了
    

    ログ形式

    プログラムは以下の形式でログを出力:

    timestamp_in_ms X has taken a fork
    timestamp_in_ms X is eating
    timestamp_in_ms X is sleeping
    timestamp_in_ms X is thinking
    timestamp_in_ms X died
    

    タイムスタンプはプログラム開始からのミリ秒。出力は競合なく行う必要がある(ログ出力もクリティカルセクション)。

    実装要件

  • デッドロック回避: プログラムは永久にブロックしてはならない
  • 餓死防止: 適切なパラメータで哲学者は死なない
  • データレース排除: 全ての共有データを適切に保護
  • 精度: ミリ秒単位のタイミング精度

1.6 時間とタイミング

POSIXの時間関数

UNIXシステムでは、gettimeofday()が標準的な高精度時刻取得関数である:

#include <sys/time.h>

long get_timestamp_ms(void)
{
    struct timeval tv;

    gettimeofday(&tv, NULL);
    return (tv.tv_sec * 1000L + tv.tv_usec / 1000L);
}

struct timeval:

  • tv_sec: 1970年1月1日からの秒数
  • tv_usec: マイクロ秒(0-999999)
  • タイミングの課題

  • システムコールのオーバーヘッド: usleep()は正確ではない
  • スケジューリングの遅延: スレッドがすぐ再開されるとは限らない
  • クロック精度: システムクロックの解像度に依存
  • 精密な待機の実装:

    void precise_sleep(long duration_ms, long start_time)
    {
        long elapsed;
    
        while (1)
        {
            elapsed = get_timestamp_ms() - start_time;
            if (elapsed >= duration_ms)
                break;
            /* ビジーウェイトを避けるため短いsleep */
            usleep(500);
        }
    }
    

    1.7 データ構造の設計

    基本構造体

    typedef struct s_fork
    {
        pthread_mutex_t mutex;
        int             id;
    }   t_fork;
    
    typedef struct s_philo
    {
        int             id;
        pthread_t       thread;
        long            last_meal_time;
        int             meals_eaten;
        t_fork          *left_fork;
        t_fork          *right_fork;
        struct s_data   *data;
    }   t_philo;
    
    typedef struct s_data
    {
        int             num_philos;
        long            time_to_die;
        long            time_to_eat;
        long            time_to_sleep;
        int             must_eat_count;
        long            start_time;
        int             simulation_end;
        pthread_mutex_t print_mutex;
        t_fork          *forks;
        t_philo         *philos;
    }   t_data;
    

    メモリレイアウト

    t_data:
    ┌─────────────────────────────────────────┐
    │ num_philos, time_to_die, etc.           │
    ├─────────────────────────────────────────┤
    │ forks* ──────→ [Fork 0][Fork 1]...[N-1] │
    ├─────────────────────────────────────────┤
    │ philos* ─────→ [Philo 0][Philo 1]...[N-1]│
    └─────────────────────────────────────────┘
    
    各哲学者:
    ┌─────────────────────────────────────────┐
    │ id, thread, last_meal_time, etc.        │
    ├─────────────────────────────────────────┤
    │ left_fork*  ──→ forks[id]               │
    │ right_fork* ──→ forks[(id+1) % n]       │
    │ data*       ──→ t_data構造体            │
    └─────────────────────────────────────────┘
    

    1.8 まとめ

    本章では、並行プログラミングの歴史と理論的基盤を学んだ:

  • 歴史的背景: CTSS、Dijkstra、Petri
  • 食事する哲学者問題: 1965年のDijkstraの原典
  • デッドロック条件: Coffmanの4条件
  • プロセスとスレッド: 概念の歴史と比較
  • 並行性と並列性: Rob Pikeの定義、Amdahl/Gustafsonの法則
  • プロジェクト仕様: 42の要件と実装方針
  • 次章では、POSIXスレッド(pthread)の理論と実装について詳しく学ぶ。

    ---

    参考文献

  • Dijkstra, E. W. (1965). "Cooperating Sequential Processes", Technical Report EWD-123
  • Coffman, E. G., Elphick, M. J., & Shoshani, A. (1971). "System Deadlocks", ACM Computing Surveys, 3(2)
  • Petri, C. A. (1962). "Kommunikation mit Automaten", PhD Thesis, University of Bonn
  • Corbató, F. J. (1963). "The Compatible Time-Sharing System: A Programmer's Guide", MIT Press
  • Pike, R. (2012). "Concurrency is not Parallelism", Heroku's Waza conference
  • Amdahl, G. M. (1967). "Validity of the single processor approach to achieving large scale computing capabilities", AFIPS Spring Joint Computer Conference
  • Gustafson, J. L. (1988). "Reevaluating Amdahl's Law", Communications of the ACM, 31(5)
  • IEEE (1995). "POSIX.1c-1995: Threads Extensions", IEEE Std 1003.1c-1995