第6章: 並行システムのテストと検証

6.1 並行システムテストの理論

Dijkstraのテスト不可能性(1969年)

Edsger W. Dijkstraは1969年に、テストの本質的な限界を指摘した("Notes on Structured Programming", 1969):

> "Program testing can be used to show the presence of bugs, but never to show their absence." > (プログラムテストはバグの存在を示すことはできるが、バグの不在を証明することは決してできない)

並行システムでは、この問題はさらに深刻である:

逐次プログラム:
  同じ入力 → 同じ出力
  テスト可能なパス数: 有限(条件分岐の組み合わせ)

並行プログラム:
  同じ入力 → 異なる出力(可能)
  可能なインターリービング: 指数的に増大
  n個のスレッドがそれぞれm命令 → (nm)! / (m!)^n 通り

Heisenbugs(1987年)

Jim GrayとAndreas Reuterは1987年に、観察によって挙動が変化するバグをHeisenbugと命名した("Transaction Processing: Concepts and Techniques", 1993年出版):

語源: 量子力学のハイゼンベルクの不確定性原理から

Heisenbugの特徴:
1. 再現が困難(または不可能)
2. デバッガで観察すると消失
3. ログ出力を追加すると消失
4. タイミング依存

原因:
- printf()やデバッガの追加 → タイミング変化
- タイミング変化 → スケジューリング変化
- スケジューリング変化 → レース条件が発現しない

Bohrbugs(対照概念):

決定的で再現可能なバグ
逐次プログラムの典型的なバグ
デバッグが容易

Beizer のテスト分類(1990年)

Boris Beizerは"Software Testing Techniques"(1990年)において、テスト技法を体系化した:

ブラックボックステスト:

内部構造を考慮しない
入力と出力の関係のみをテスト
境界値分析、同値分割

ホワイトボックステスト:

内部構造を考慮する
コードパスのカバレッジ
条件カバレッジ、分岐カバレッジ

並行システムでの追加考慮:

1. インターリービングカバレッジ
2. 同期ポイントのテスト
3. デッドロック条件の網羅
4. レース条件の誘発

6.2 形式的検証

形式的手法の歴史

形式的検証は、数学的手法でプログラムの正しさを証明する分野である。

Floyd-Hoare論理(1969年):

前提条件と事後条件を用いた証明
{P} S {Q}
Pが成り立つ状態でSを実行すると、Qが成り立つ

TLA+(1994年):

Leslie Lamportが開発したTLA+(Temporal Logic of Actions)は、並行・分散システムの仕様記述と検証のための言語である:

TLA+ での食事する哲学者問題の仕様(簡略版):

---- MODULE DiningPhilosophers ----
CONSTANT N
VARIABLES state, forks

TypeOK ==
    /\ state \in [1..N -> {"thinking", "hungry", "eating"}]
    /\ forks \in [0..N-1 -> {"free", "taken"}]

Init ==
    /\ state = [i \in 1..N |-> "thinking"]
    /\ forks = [i \in 0..N-1 |-> "free"]

TakeLeft(i) ==
    /\ state[i] = "hungry"
    /\ forks[i-1] = "free"
    /\ forks' = [forks EXCEPT ![i-1] = "taken"]
    /\ UNCHANGED state

Eat(i) ==
    /\ state[i] = "hungry"
    /\ forks[i-1] = "taken"
    /\ forks[i % N] = "taken"
    /\ state' = [state EXCEPT ![i] = "eating"]
    /\ UNCHANGED forks

NoDeadlock == \E i \in 1..N: ENABLED(Eat(i))
====

モデル検査(Model Checking)

Edmund M. ClarkeとE. Allen Emersonは1981年にモデル検査を提案した(1981年、2007年チューリング賞受賞):

モデル検査の原理:
1. システムを有限状態機械としてモデル化
2. 検証したい性質を時相論理で記述
3. 全ての状態を探索
4. 性質違反があれば反例を出力

利点:
- 自動化された検証
- 反例(デバッグ情報)の生成

欠点:
- 状態爆発(State Explosion)
- 大規模システムへの適用困難

SPINモデルチェッカー(Gerard J. Holzmann, 1991):

/* Promela での食事する哲学者問題 */

#define N 5

byte fork[N] = 0;

proctype Philosopher(byte id) {
    byte left = id;
    byte right = (id + 1) % N;

    do
    :: /* thinking */
       atomic {
           fork[left] == 0 -> fork[left] = 1
       };
       atomic {
           fork[right] == 0 -> fork[right] = 1
       };
       /* eating */
       fork[left] = 0;
       fork[right] = 0
    od
}

init {
    byte i;
    for (i : 0 .. N-1) {
        run Philosopher(i)
    }
}

/* 検証したい性質 */
ltl no_deadlock { []<>somePhiloEating }

6.3 動的解析ツールの理論

Valgrindのアーキテクチャ

Nicholas Nethercoteらが開発したValgrind(2002年)は、動的バイナリ計装フレームワークである:

Valgrindの動作原理:

1. プログラムをVEX中間表現に変換
2. 計装コードを挿入
3. 変換されたコードを実行
4. 各ツールが特定の解析を実行

ツール:
- Memcheck: メモリエラー検出
- Helgrind: データレース検出
- DRD: データレース検出(別実装)
- Callgrind: プロファイリング

Helgrindのデータレース検出アルゴリズム:

Helgrindは、Happens-Before関係を追跡してデータレースを検出する:

各メモリアクセスについて:
1. アクセスの種類(読み取り/書き込み)を記録
2. アクセス時のロック状態を記録
3. スレッドのベクタークロックを更新
4. 前回のアクセスとの順序関係をチェック

順序付けがない場合 → データレース報告

ThreadSanitizerのアルゴリズム

Google開発のThreadSanitizer(TSan)は、コンパイル時計装による高速なデータレース検出を実現する:

TSanの最適化:
1. シャドウメモリ: 各メモリ位置の最近のアクセスを記録
2. ベクタークロック圧縮: 効率的なHappens-Before追跡
3. ロックセット: 保持ロックの集合を追跡

コンパイル時計装:
- 全てのメモリアクセス前にチェックコードを挿入
- ロック取得/解放をフック
- スレッド作成/合流をフック

TSanの使用:

# コンパイル
gcc -fsanitize=thread -g -O1 philo.c -lpthread -o philo

# 実行
./philo 5 800 200 200

# 典型的な出力
# WARNING: ThreadSanitizer: data race (pid=12345)
#   Write of size 4 at 0x... by thread T1:
#     #0 set_death philo.c:156
#   Previous read of size 4 at 0x... by thread T2:
#     #0 check_death philo.c:123

アドレスサニタイザ(ASan)

AddressSanitizerは、メモリエラー検出ツールである:

検出可能なエラー:
- ヒープバッファオーバーフロー
- スタックバッファオーバーフロー
- use-after-free
- double-free
- メモリリーク

動作原理:
- レッドゾーン: バッファ周囲に「毒」領域を配置
- シャドウメモリ: 各バイトの有効性を追跡
- フック: malloc/freeを置き換え

6.4 ストレステストの理論

負荷テストの原則

リトルの法則(1961年):

John D.C. Littleが証明した待ち行列理論の基本定理:

L = λ × W

L: システム内の平均アイテム数
λ: 到着率(単位時間あたりの到着数)
W: 平均滞在時間

並行システムへの適用:

食事する哲学者問題:
- L: 食事中の平均哲学者数
- λ: 食事開始の頻度
- W: 食事時間

安定条件: λ × W < N/2
(N人の哲学者がN本のフォークを共有)

ストレステストのパターン

負荷増加テスト:

スレッド数を段階的に増加
1 → 2 → 5 → 10 → 50 → 100 → 200

各段階で:
- スループットを測定
- レイテンシを測定
- エラー率を測定

持続負荷テスト:

一定の負荷を長時間維持
目的:
- メモリリークの検出
- リソース枯渇の検出
- 長期的な安定性確認

スパイクテスト:

急激な負荷変動
低負荷 → 高負荷 → 低負荷
目的:
- システムの回復力確認
- バッファリングの検証

6.5 Philosophersのテスト戦略

機能テストケース

基本機能テスト:

#!/bin/bash
# test_basic.sh

RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'

test_case() {
    local name="$1"
    local args="$2"
    local timeout="$3"
    local expect_death="$4"

    echo -n "Testing: $name... "

    output=$(timeout "$timeout" ./philo $args 2>&1)
    exit_code=$?

    if [ $exit_code -eq 124 ]; then
        if [ "$expect_death" = "no" ]; then
            echo -e "${GREEN}PASS${NC} (no death, timeout expected)"
        else
            echo -e "${RED}FAIL${NC} (timeout)"
        fi
        return
    fi

    if echo "$output" | grep -q "died"; then
        if [ "$expect_death" = "yes" ]; then
            echo -e "${GREEN}PASS${NC} (death detected)"
        else
            echo -e "${RED}FAIL${NC} (unexpected death)"
            echo "$output" | grep "died"
        fi
    else
        if [ "$expect_death" = "no" ]; then
            echo -e "${GREEN}PASS${NC} (no death)"
        else
            echo -e "${RED}FAIL${NC} (expected death)"
        fi
    fi
}

echo "=== Basic Tests ==="
test_case "No death (generous time)" "5 800 200 200" 3 "no"
test_case "One philosopher dies" "1 400 200 200" 2 "yes"
test_case "Tight timing survives" "4 410 200 200" 5 "no"
test_case "Too tight dies" "4 310 200 100" 2 "yes"
test_case "Two philosophers" "2 800 200 200" 3 "no"

食事回数テスト:

echo "=== Must Eat Tests ==="

test_must_eat() {
    local philos="$1"
    local must_eat="$2"

    echo -n "Testing: $philos philosophers, $must_eat meals each... "

    output=$(timeout 10 ./philo $philos 800 200 200 $must_eat 2>&1)

    # 各哲学者が指定回数食べたか確認
    for i in $(seq 1 $philos); do
        eat_count=$(echo "$output" | grep " $i is eating" | wc -l)
        if [ $eat_count -lt $must_eat ]; then
            echo -e "${RED}FAIL${NC} (philosopher $i ate $eat_count times)"
            return
        fi
    done

    if echo "$output" | grep -q "died"; then
        echo -e "${RED}FAIL${NC} (unexpected death)"
    else
        echo -e "${GREEN}PASS${NC}"
    fi
}

test_must_eat 5 7
test_must_eat 4 10
test_must_eat 100 3

タイミング検証

#!/bin/bash
# test_timing.sh

check_death_timing() {
    local time_to_die="$1"
    local tolerance="$2"  # 許容誤差(ms)

    echo -n "Testing death timing (time_to_die=$time_to_die)... "

    output=$(timeout 5 ./philo 1 $time_to_die 200 200 2>&1)

    death_line=$(echo "$output" | grep "died")
    death_time=$(echo "$death_line" | awk '{print $1}')

    if [ -z "$death_time" ]; then
        echo -e "${RED}FAIL${NC} (no death detected)"
        return
    fi

    min_time=$((time_to_die))
    max_time=$((time_to_die + tolerance))

    if [ "$death_time" -ge "$min_time" ] && [ "$death_time" -le "$max_time" ]; then
        echo -e "${GREEN}PASS${NC} (died at ${death_time}ms)"
    else
        echo -e "${RED}FAIL${NC} (died at ${death_time}ms, expected ${min_time}-${max_time})"
    fi
}

echo "=== Timing Tests ==="
check_death_timing 100 15
check_death_timing 200 15
check_death_timing 500 15
check_death_timing 1000 20

データレーステスト

#!/bin/bash
# test_races.sh

echo "=== Data Race Detection with ThreadSanitizer ==="

# TSan付きでコンパイル
echo "Compiling with ThreadSanitizer..."
gcc -fsanitize=thread -g -O1 -pthread srcs/*.c -o philo_tsan 2>/dev/null

if [ $? -ne 0 ]; then
    echo "TSan compilation failed (may not be supported)"
    exit 1
fi

echo "Running tests..."

# 複数回実行(レース条件は確率的)
for i in {1..10}; do
    echo -n "Run $i: "
    output=$(timeout 3 ./philo_tsan 5 800 200 200 2>&1)

    if echo "$output" | grep -q "ThreadSanitizer: data race"; then
        echo -e "${RED}DATA RACE DETECTED${NC}"
        echo "$output" | grep -A 10 "ThreadSanitizer"
        exit 1
    else
        echo -e "${GREEN}clean${NC}"
    fi
done

echo "All runs passed without data races"

メモリリークテスト

#!/bin/bash
# test_memory.sh

echo "=== Memory Leak Detection with Valgrind ==="

valgrind --leak-check=full --error-exitcode=1 \
    --suppressions=/dev/null \
    ./philo 5 800 200 200 5 2>&1 | tee valgrind_output.txt

if grep -q "All heap blocks were freed" valgrind_output.txt; then
    echo -e "${GREEN}No memory leaks${NC}"
else
    echo -e "${RED}Memory leaks detected${NC}"
fi

# Helgrind でデータレースチェック
echo ""
echo "=== Data Race Detection with Helgrind ==="

valgrind --tool=helgrind --error-exitcode=1 \
    ./philo 5 800 200 200 3 2>&1 | tee helgrind_output.txt

if grep -q "ERROR SUMMARY: 0 errors" helgrind_output.txt; then
    echo -e "${GREEN}No data races${NC}"
else
    echo -e "${RED}Possible data races detected${NC}"
fi

6.6 デッドロック検出と回避

動的デッドロック検出

待機グラフ(Wait-for Graph)分析:

/* 動的デッドロック検出の実装概念 */

typedef struct s_wait_graph {
    int         thread_count;
    int         **edges;       /* edges[i][j] = 1: thread i waits for thread j */
    pthread_mutex_t mutex;
}   t_wait_graph;

/* サイクル検出(深さ優先探索) */
int has_cycle(t_wait_graph *graph)
{
    int *visited = calloc(graph->thread_count, sizeof(int));
    int *rec_stack = calloc(graph->thread_count, sizeof(int));
    int result = 0;

    for (int i = 0; i < graph->thread_count; i++)
    {
        if (!visited[i] && dfs_cycle(graph, i, visited, rec_stack))
        {
            result = 1;
            break;
        }
    }

    free(visited);
    free(rec_stack);
    return (result);
}

/* ロック取得時にグラフを更新 */
void before_lock(t_wait_graph *graph, int thread_id, int lock_owner)
{
    pthread_mutex_lock(&graph->mutex);
    graph->edges[thread_id][lock_owner] = 1;

    if (has_cycle(graph))
        printf("DEADLOCK DETECTED!\n");

    pthread_mutex_unlock(&graph->mutex);
}

静的デッドロック検出

コンパイル時にロック取得順序を分析:

ロック順序グラフ:
1. 全てのロック取得パターンを抽出
2. ロック間の順序関係をグラフ化
3. サイクルを検出

例:
関数A: lock(m1); lock(m2); unlock(m2); unlock(m1);
関数B: lock(m2); lock(m1); unlock(m1); unlock(m2);

→ m1 → m2 と m2 → m1 のエッジ存在
→ サイクル検出 → デッドロックの可能性

Philosophersでのデッドロック回避実装

/* 戦略1: リソース順序付け */
int take_forks_ordered(t_philo *philo)
{
    int first, second;

    /* 常にIDの小さいフォークから取得 */
    if (philo->left_fork_id < philo->right_fork_id)
    {
        first = philo->left_fork_id;
        second = philo->right_fork_id;
    }
    else
    {
        first = philo->right_fork_id;
        second = philo->left_fork_id;
    }

    pthread_mutex_lock(&philo->data->forks[first]);
    if (check_simulation_end(philo->data))
    {
        pthread_mutex_unlock(&philo->data->forks[first]);
        return (FALSE);
    }
    print_status(philo, "has taken a fork");

    pthread_mutex_lock(&philo->data->forks[second]);
    if (check_simulation_end(philo->data))
    {
        pthread_mutex_unlock(&philo->data->forks[second]);
        pthread_mutex_unlock(&philo->data->forks[first]);
        return (FALSE);
    }
    print_status(philo, "has taken a fork");

    return (TRUE);
}

/* 戦略2: 開始時刻のずらし */
void *philosopher_routine(void *arg)
{
    t_philo *philo = (t_philo *)arg;

    /* 偶数IDは開始を遅延 */
    if (philo->id % 2 == 0)
        precise_sleep(philo->data->time_to_eat / 2);

    /* ループ開始 */
    while (!check_simulation_end(philo->data))
    {
        /* ... */
    }
    return (NULL);
}

6.7 GDBによる並行プログラムのデバッグ

マルチスレッドデバッグの基本

# デバッグシンボル付きコンパイル
gcc -g -pthread srcs/*.c -o philo

# GDB起動
gdb ./philo

# GDB内コマンド
(gdb) set args 5 800 200 200
(gdb) break philosopher_routine
(gdb) run

スレッド操作

# 全スレッド表示
(gdb) info threads

# 出力例:
#   Id   Target Id         Frame
# * 1    Thread 0x7... (LWP 12345) "philo" in main at main.c:10
#   2    Thread 0x7... (LWP 12346) "philo" philosopher_routine at routine.c:25
#   3    Thread 0x7... (LWP 12347) "philo" philosopher_routine at routine.c:30

# スレッド切り替え
(gdb) thread 2

# 全スレッドのバックトレース
(gdb) thread apply all bt

# 特定のスレッドでコマンド実行
(gdb) thread apply 2-5 print philo->id

ミューテックスのデバッグ

# ミューテックスの状態確認
(gdb) print data->forks[0]

# ロックの所有者を確認(glibc実装依存)
(gdb) print data->forks[0].__data.__owner

# ブレークポイントで停止後、ミューテックスを解放
(gdb) call pthread_mutex_unlock(&data->forks[0])

非停止デバッグモード

# 非停止モード有効化(他のスレッドを停止しない)
(gdb) set target-async on
(gdb) set non-stop on

# 個別スレッドの続行
(gdb) continue &

# スレッドを停止
(gdb) interrupt -a

6.8 出力検証

ログ解析スクリプト

#!/usr/bin/env python3
# validate_philo_output.py

import sys
import re
from collections import defaultdict

def parse_line(line):
    """ログ行をパース"""
    match = re.match(r'^(\d+)\s+(\d+)\s+(.+)

継続的テスト

#!/bin/bash
# continuous_test.sh

RUNS=100
FAILURES=0

echo "Running $RUNS iterations..."

for i in $(seq 1 $RUNS); do
    # 進捗表示
    printf "\rRun %d/%d" $i $RUNS

    # 実行と出力キャプチャ
    timeout 5 ./philo 5 800 200 200 5 > output.txt 2>&1

    # 検証
    python3 validate_philo_output.py output.txt 800 > /dev/null 2>&1
    if [ $? -ne 0 ]; then
        FAILURES=$((FAILURES + 1))
        echo ""
        echo "Failure detected at run $i"
    fi

    # データレースチェック(オプション)
    if [ "$CHECK_RACES" = "1" ]; then
        timeout 5 ./philo_tsan 5 800 200 200 5 > /dev/null 2>&1
        if [ $? -ne 0 ]; then
            echo ""
            echo "Race detected at run $i"
        fi
    fi
done

echo ""
echo "Results: $((RUNS - FAILURES))/$RUNS passed, $FAILURES failures"

6.9 パフォーマンステスト

スループット測定

/* パフォーマンス測定用の計装 */

typedef struct s_perf_counters {
    atomic_long total_meals;
    atomic_long total_wait_time;
    long        start_time;
    long        end_time;
}   t_perf_counters;

void philo_eat(t_philo *philo)
{
    long before_eat = get_timestamp_ms();

    print_status(philo, "is eating");

    pthread_mutex_lock(&philo->data->meal_mutex);
    philo->last_meal_time = get_timestamp_ms();
    philo->meals_eaten++;
    pthread_mutex_unlock(&philo->data->meal_mutex);

    /* パフォーマンスカウンタ更新 */
    atomic_fetch_add(&philo->data->perf->total_meals, 1);

    precise_sleep(philo->data->time_to_eat);
}

void print_performance_stats(t_data *data)
{
    long duration = data->perf->end_time - data->perf->start_time;
    long total_meals = atomic_load(&data->perf->total_meals);

    printf("\n=== Performance Statistics ===\n");
    printf("Duration: %ld ms\n", duration);
    printf("Total meals: %ld\n", total_meals);
    printf("Throughput: %.2f meals/sec\n",
           (double)total_meals / duration * 1000);
    printf("Avg meals per philosopher: %.2f\n",
           (double)total_meals / data->num_philos);
}

ベンチマークスクリプト

#!/bin/bash
# benchmark.sh

echo "=== Philosopher Benchmark ==="
echo ""

benchmark() {
    local philos=$1
    local time=$2
    local must_eat=$3

    echo "Config: $philos philosophers, $must_eat meals each"

    start=$(date +%s%N)
    ./philo $philos $time 200 200 $must_eat > /dev/null 2>&1
    end=$(date +%s%N)

    duration=$(( (end - start) / 1000000 ))  # ミリ秒
    total_meals=$(( philos * must_eat ))
    throughput=$(echo "scale=2; $total_meals / $duration * 1000" | bc)

    echo "  Duration: ${duration}ms"
    echo "  Throughput: ${throughput} meals/sec"
    echo ""
}

benchmark 5 800 10
benchmark 10 800 10
benchmark 50 800 10
benchmark 100 800 5
benchmark 200 800 3

6.10 まとめ

本章では、並行システムのテストと検証について学んだ:

  • テスト理論: Dijkstraのテスト限界、Heisenbugs
  • 形式的検証: TLA+、モデル検査
  • 動的解析: Valgrind、ThreadSanitizer
  • ストレステスト: 負荷テストの原則、リトルの法則
  • 機能テスト: タイミング検証、データレース検出
  • デッドロック: 検出と回避戦略
  • GDBデバッグ: マルチスレッドデバッグ技法
  • 出力検証: ログ解析、継続テスト
  • 並行プログラムのテストは、その非決定性により本質的に困難である。しかし、適切なツールと体系的なアプローチにより、高品質な並行ソフトウェアを開発することは可能である。

    ---

    参考文献

  • Dijkstra, E. W. (1969). "Notes on Structured Programming", Technical Report EWD-249
  • Gray, J. & Reuter, A. (1993). "Transaction Processing: Concepts and Techniques", Morgan Kaufmann
  • Beizer, B. (1990). "Software Testing Techniques", 2nd Edition, Van Nostrand Reinhold
  • Clarke, E. M., Emerson, E. A., & Sifakis, J. (2009). "Model Checking: Algorithmic Verification and Debugging", Communications of the ACM, 52(11)
  • Lamport, L. (1994). "The Temporal Logic of Actions", ACM TOPLAS, 16(3)
  • Nethercote, N. & Seward, J. (2007). "Valgrind: A Framework for Heavyweight Dynamic Binary Instrumentation", PLDI 2007
  • Serebryany, K. & Iskhodzhanov, T. (2009). "ThreadSanitizer: Data Race Detection in Practice", WBIA 2009
  • Holzmann, G. J. (2004). "The SPIN Model Checker", Addison-Wesley
  • Savage, S., et al. (1997). "Eraser: A Dynamic Data Race Detector for Multithreaded Programs", ACM TOCS, 15(4)
  • Downey, A. B. (2016). "The Little Book of Semaphores", Green Tea Press

, line.strip()) if not match: return None return { 'timestamp': int(match.group(1)), 'philo_id': int(match.group(2)), 'action': match.group(3) } def validate_output(filename, time_to_die): """出力を検証""" with open(filename, 'r') as f: lines = f.readlines() events = [] errors = [] for i, line in enumerate(lines): parsed = parse_line(line) if not parsed: errors.append(f"Line {i+1}: Invalid format: {line.strip()}") continue parsed['line_num'] = i + 1 events.append(parsed) # タイムスタンプの単調増加チェック prev_ts = -1 for event in events: if event['timestamp'] < prev_ts: errors.append(f"Line {event['line_num']}: Timestamp decreased") prev_ts = event['timestamp'] # 死亡後の出力チェック death_found = False for event in events: if death_found and event['action'] != 'died': errors.append(f"Line {event['line_num']}: Output after death") if event['action'] == 'died': death_found = True # 状態遷移の検証 philo_states = defaultdict(list) for event in events: philo_states[event['philo_id']].append(event) for philo_id, state_events in philo_states.items(): last_meal_time = 0 for event in state_events: if event['action'] == 'is eating': # 死亡までの時間チェック if event['timestamp'] - last_meal_time > time_to_die: errors.append( f"Philosopher {philo_id} ate after time_to_die " f"(gap: {event['timestamp'] - last_meal_time}ms)" ) last_meal_time = event['timestamp'] return errors if __name__ == "__main__": if len(sys.argv) != 3: print("Usage: python3 validate.py <output_file> <time_to_die>") sys.exit(1) errors = validate_output(sys.argv[1], int(sys.argv[2])) if errors: print("Validation FAILED:") for error in errors[:10]: # 最初の10個のエラー print(f" - {error}") if len(errors) > 10: print(f" ... and {len(errors) - 10} more errors") sys.exit(1) else: print("Validation PASSED") sys.exit(0)

継続的テスト

___CODE_BLOCK_34___

6.9 パフォーマンステスト

スループット測定

___CODE_BLOCK_35___

ベンチマークスクリプト

___CODE_BLOCK_36___

6.10 まとめ

本章では、並行システムのテストと検証について学んだ:

  • テスト理論: Dijkstraのテスト限界、Heisenbugs
  • 形式的検証: TLA+、モデル検査
  • 動的解析: Valgrind、ThreadSanitizer
  • ストレステスト: 負荷テストの原則、リトルの法則
  • 機能テスト: タイミング検証、データレース検出
  • デッドロック: 検出と回避戦略
  • GDBデバッグ: マルチスレッドデバッグ技法
  • 出力検証: ログ解析、継続テスト
  • 並行プログラムのテストは、その非決定性により本質的に困難である。しかし、適切なツールと体系的なアプローチにより、高品質な並行ソフトウェアを開発することは可能である。

    ---

    参考文献

  • Dijkstra, E. W. (1969). "Notes on Structured Programming", Technical Report EWD-249
  • Gray, J. & Reuter, A. (1993). "Transaction Processing: Concepts and Techniques", Morgan Kaufmann
  • Beizer, B. (1990). "Software Testing Techniques", 2nd Edition, Van Nostrand Reinhold
  • Clarke, E. M., Emerson, E. A., & Sifakis, J. (2009). "Model Checking: Algorithmic Verification and Debugging", Communications of the ACM, 52(11)
  • Lamport, L. (1994). "The Temporal Logic of Actions", ACM TOPLAS, 16(3)
  • Nethercote, N. & Seward, J. (2007). "Valgrind: A Framework for Heavyweight Dynamic Binary Instrumentation", PLDI 2007
  • Serebryany, K. & Iskhodzhanov, T. (2009). "ThreadSanitizer: Data Race Detection in Practice", WBIA 2009
  • Holzmann, G. J. (2004). "The SPIN Model Checker", Addison-Wesley
  • Savage, S., et al. (1997). "Eraser: A Dynamic Data Race Detector for Multithreaded Programs", ACM TOCS, 15(4)
  • Downey, A. B. (2016). "The Little Book of Semaphores", Green Tea Press