Skip to content
Published on

並行性と並列性完全攻略:マルチスレッド、非同期、イベントループのすべて

Authors

はじめに:並行性(へいこうせい)はなぜ難(むずか)しいのか?

2024年(ねん)のAWS障害(しょうがい)の主要(しゅよう)な原因(げんいん)の1つは並行性(へいこうせい)バグでした。Cloudflareの大規模(だいきぼ)障害(しょうがい)もレースコンディションから発生(はっせい)しました。並行性(へいこうせい)プログラミングは正(ただ)しく理解(りかい)しないと、デバッグ不可能(ふかのう)なバグを生(う)み出(だ)します。

Rob Pike(Go言語共同(げんごきょうどう)創設者(そうせつしゃ))は明確(めいかく)に区別(くべつ)しました:

"並行性(Concurrency)は独立して実行されるタスクを扱うこと、
 並列性(Parallelism)は複数のタスクを同時に実行すること。
 並行性は構造(structure)に関すること、
 並列性は実行(execution)に関すること。"
Rob Pike
並行性 (Concurrency):            並列性 (Parallelism):
一人が二つの仕事を交互に         二人がそれぞれ一つの仕事を同時に

   Task A ━━━━━━╸               Task A ━━━━━━━━━━━━
Task B ━━━━━━━━━━━━
   Task B ━━━━━━╸
CPU Core 1 ━━━━━━━━
   Task A ━━━━━━╸               CPU Core 2 ━━━━━━━━
   CPU Core 1 ━━━━━━━━
   (タイムスライシング)          (物理的に同時実行)

このガイドでは、並行性(へいこうせい)と並列性(へいれつせい)の理論的(りろんてき)基礎(きそ)から言語別(げんごべつ)の実装(じっそう)、実践(じっせん)パターン、そしてよくあるバグと解決策(かいけつさく)まで体系的(たいけいてき)にカバーします。


1. スレッディングモデル

1.1 1:1モデル(カーネルスレッド)

OSが直接(ちょくせつ)管理(かんり)するスレッド。各(かく)ユーザースレッドが1つのカーネルスレッドにマッピングされます。

ユーザースレッド: T1    T2    T3    T4
                 │      │      │      │
カーネルスレッド: KT1   KT2   KT3   KT4
                 │      │      │      │
CPUコア:        Core1  Core2  Core1  Core2

使用言語(しようげんご): Java(伝統的(でんとうてき))、C/C++(pthread)、Rust

1.2 N:1モデル(グリーンスレッド)

ランタイムが複数(ふくすう)のユーザースレッドを1つのOSスレッドにマッピングします。

ユーザースレッド: T1  T2  T3  T4  T5  T6
                 └──┬──┘  └──┬──┘  └──┬──┘
                    │        │        │
カーネルスレッド:   KT1      KT1      KT1
CPUコア:           Core1

メリット: 軽量(けいりょう)、高速(こうそく)コンテキストスイッチ デメリット: マルチコア活用(かつよう)不可(ふか)

1.3 M:Nモデル(ハイブリッド)

M個(こ)のユーザースレッドをN個(こ)のOSスレッドにマッピングする最適(さいてき)モデル:

ユーザースレッド: G1  G2  G3  G4  G5  G6  G7  G8
                 └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘
                    │       │       │       │
カーネルスレッド:   KT1     KT2     KT3     KT4
                    │       │       │       │
CPUコア:           Core1   Core2   Core3   Core4

使用言語(しようげんご): Go(ゴルーチン)、Java 21+(Virtual Thread)、Erlang(プロセス)

1.4 スレッディングモデル比較(ひかく)

┌─────────────────┬──────────────┬──────────────┬──────────────┐
│ 項目            │ 1:1N:1M:N├─────────────────┼──────────────┼──────────────┼──────────────┤
│ スレッドコスト  │ ~1MBスタック │ ~KBスタック  │ ~KBスタック  │
│ 生成速度        │ 遅い(~ms)速い(~us)速い(~us)│ コンテキスト    │ 高い(カーネル)低い(ユーザー)低い(ユーザー)│ マルチコア      │ OXOI/Oブロッキング │ スレッドのみ │ 全体ブロック │ スレッドのみ │
│ 最大スレッド数  │ ~数千        │ ~数百万      │ ~数百万      │
│ 例              │ Java, C++    │ 初期Ruby     │ Go, Erlang└─────────────────┴──────────────┴──────────────┴──────────────┘

2. 言語別(げんごべつ)並行性(へいこうせい)比較(ひかく)

2.1 Go — ゴルーチンとチャネル

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    results := make(chan string, 10)

    urls := []string{
        "https://api.example.com/users",
        "https://api.example.com/orders",
        "https://api.example.com/products",
    }

    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            results <- fmt.Sprintf("Fetched: %s", u)
        }(url)
    }

    go func() {
        wg.Wait()
        close(results)
    }()

    for result := range results {
        fmt.Println(result)
    }
}

特徴(とくちょう): CSP(Communicating Sequential Processes)ベース。「メモリを共有(きょうゆう)するのではなく、通信(つうしん)でメモリを共有(きょうゆう)せよ。」

2.2 Java 21+ — Virtual Thread

import java.util.concurrent.*;
import java.util.List;

public class VirtualThreadExample {
    public static void main(String[] args) throws Exception {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<Future<String>> futures = List.of(
                executor.submit(() -> fetchURL("https://api.example.com/users")),
                executor.submit(() -> fetchURL("https://api.example.com/orders")),
                executor.submit(() -> fetchURL("https://api.example.com/products"))
            );

            for (var future : futures) {
                System.out.println(future.get());
            }
        }

        // Structured Concurrency (Preview)
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            var user = scope.fork(() -> fetchUser(1));
            var order = scope.fork(() -> fetchOrder(1));

            scope.join();
            scope.throwIfFailed();

            System.out.println(user.get() + ", " + order.get());
        }
    }
}

特徴(とくちょう): Project Loomで追加(ついか)された仮想(かそう)スレッド。既存(きそん)コードの変更(へんこう)を最小限(さいしょうげん)に数百万(すうひゃくまん)の同時(どうじ)タスク処理(しょり)。

2.3 Python — asyncioとGIL

import asyncio
import aiohttp

async def fetch_url(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    urls = [
        "https://api.example.com/users",
        "https://api.example.com/orders",
        "https://api.example.com/products",
    ]

    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks)

        for url, result in zip(urls, results):
            print(f"Fetched {url}: {len(result)} bytes")

asyncio.run(main())

特徴(とくちょう): GIL(Global Interpreter Lock)のためCPUバウンド作業(さぎょう)は真(しん)の並列処理(へいれつしょり)不可(ふか)。I/Oバウンドにはasyncioが効果的(こうかてき)。CPUバウンドにはmultiprocessing使用(しよう)。

2.4 Rust — tokio非同期(ひどうき)ランタイム

use tokio;
use reqwest;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let urls = vec![
        "https://api.example.com/users",
        "https://api.example.com/orders",
        "https://api.example.com/products",
    ];

    let mut handles = vec![];

    for url in urls {
        let handle = tokio::spawn(async move {
            let resp = reqwest::get(url).await?;
            let body = resp.text().await?;
            Ok::<String, reqwest::Error>(body)
        });
        handles.push(handle);
    }

    for handle in handles {
        match handle.await? {
            Ok(body) => println!("Fetched: {} bytes", body.len()),
            Err(e) => eprintln!("Error: {}", e),
        }
    }

    Ok(())
}

特徴(とくちょう): 所有権(しょゆうけん)システムでコンパイル時(じ)にデータレースを防止(ぼうし)。Send/Syncトレイトでスレッド安全性(あんぜんせい)を保証(ほしょう)。

2.5 JavaScript — イベントループ

async function fetchAll() {
    const urls = [
        'https://api.example.com/users',
        'https://api.example.com/orders',
        'https://api.example.com/products',
    ];

    // Promise.allで同時実行
    const results = await Promise.all(
        urls.map(url => fetch(url).then(r => r.json()))
    );

    // Promise.allSettledで失敗許容
    const settled = await Promise.allSettled(
        urls.map(url => fetch(url).then(r => r.json()))
    );

    settled.forEach((result, i) => {
        if (result.status === 'fulfilled') {
            console.log(`Success: ${urls[i]}`);
        } else {
            console.log(`Failed: ${urls[i]} - ${result.reason}`);
        }
    });
}

特徴(とくちょう): シングルスレッド + イベントループ。CPUバウンド作業(さぎょう)にはWorker Threadが必要(ひつよう)。I/Oに最適化(さいてきか)。

2.6 Erlang/Elixir — アクターモデル

%% Erlangプロセス(軽量アクター)
-module(counter).
-export([start/0, increment/1, get/1]).

start() ->
    spawn(fun() -> loop(0) end).

loop(Count) ->
    receive
        {increment, From} ->
            From ! {ok, Count + 1},
            loop(Count + 1);
        {get, From} ->
            From ! {value, Count},
            loop(Count);
        stop ->
            ok
    end.

increment(Pid) ->
    Pid ! {increment, self()},
    receive {ok, NewCount} -> NewCount end.

get(Pid) ->
    Pid ! {get, self()},
    receive {value, Count} -> Count end.

特徴(とくちょう): プロセスあたり約(やく)300バイト。数百万(すうひゃくまん)のプロセスを同時(どうじ)実行(じっこう)。「Let it crash」哲学(てつがく)で耐障害性(たいしょうがいせい)を実現(じつげん)。

2.7 言語別(げんごべつ)比較表(ひかくひょう)

┌─────────────┬─────────────┬───────────────┬──────────────┬──────────────┐
│ 言語        │ モデル      │ 単位          │ スケジュール │ 特徴         │
├─────────────┼─────────────┼───────────────┼──────────────┼──────────────┤
GoCSP (M:N)   │ ゴルーチン    │ ランタイム   │ チャネル通信 │
Java 21+M:NVirtual Thread│ JVM          │ 後方互換     │
Python      │ イベントルー│ コルーチン    │ asyncio      │ GIL制約      │
Rust        │ async/awaitFuture/Task   │ tokio等      │ ゼロコスト   │
JavaScript  │ イベントルー│ Promise       │ libuv/V8     │ シングル     │
ErlangActor(M:N)  │ プロセス      │ BEAM VM      │ 耐障害性     │
Kotlin      │ コルーチン  │ CoroutineDispatcher   │ 構造的同時性 │
└─────────────┴─────────────┴───────────────┴──────────────┴──────────────┘

3. イベントループ深掘(ふかぼ)り

3.1 Node.jsイベントループフェーズ

┌───────────────────────────┐
│         timers            │  ← setTimeout, setInterval
    (タイマーCB実行)├───────────────────────────┤
│     pending callbacks     │  ← システムエラーCB
├───────────────────────────┤
│       idle, prepare       │  ← 内部使用
├───────────────────────────┤
│          poll             │  ← I/O CB(ほとんどのCB    (新しいI/Oイベント待機)├───────────────────────────┤
│          check            │  ← setImmediate CB
├───────────────────────────┤
│     close callbacks       │  ← socket.on('close')
└───────────────────────────┘
   各フェーズ間: マイクロタスクキュー処理
   (Promise.then, queueMicrotask, process.nextTick)

3.2 マイクロタスク vs マクロタスク

console.log('1: sync');

setTimeout(() => console.log('2: macro (setTimeout)'), 0);

Promise.resolve().then(() => console.log('3: micro (Promise)'));

queueMicrotask(() => console.log('4: micro (queueMicrotask)'));

console.log('5: sync');

// 出力順序:
// 1: sync
// 5: sync
// 3: micro (Promise)
// 4: micro (queueMicrotask)
// 2: macro (setTimeout)

ルール: 同期(どうき)コードが最初(さいしょ)に実行(じっこう)→マイクロタスク全部(ぜんぶ)実行(じっこう)→マクロタスク1つ実行(じっこう)→繰(く)り返(かえ)し

3.3 libuvアーキテクチャ

┌──────────────────────────────────────────┐
Node.js│  ┌─────────────────────────────────┐     │
│  │     JavaScript (V8 Engine)      │     │
│  │  ┌───────────┐  ┌────────────┐  │     │
│  │  │  Async    │  │ Event      │  │     │
│  │  │  APIs     │  │ Queue      │  │     │
│  │  └─────┬─────┘  └──────┬─────┘  │     │
│  └────────┼────────────────┼────────┘     │
│           │   Event Loop   │              │
│  ┌────────▼────────────────▼────────┐     │
│  │           libuv                   │     │
│  │  ┌──────────┐  ┌──────────────┐  │     │
│  │  │ Thread   │  │ epoll/kqueue │  │     │
│  │  │ Pool     │  │ /IOCP        │  │     │
│  │   (4 thds) (non-block)  │  │     │
│  │  └──────────┘  └──────────────┘  │     │
│  └───────────────────────────────────┘     │
│           │                │               │
File I/O          Network I/ODNS lookup        TCP/UDP└──────────────────────────────────────────┘

ファイルI/OとDNSはスレッドプールで処理(しょり)され、ネットワークI/OはOSの非同期(ひどうき)メカニズム(epoll、kqueue、IOCP)で処理(しょり)されます。


4. コルーチンと構造的(こうぞうてき)同時性(どうじせい)

4.1 Python async/await

import asyncio

async def process_item(item: str) -> str:
    await asyncio.sleep(1)
    return f"Processed: {item}"

async def main():
    items = ["a", "b", "c", "d", "e"]

    # 同時実行
    tasks = [process_item(item) for item in items]
    results = await asyncio.gather(*tasks)
    print(results)  # 1秒で全完了

    # タイムアウト
    try:
        result = await asyncio.wait_for(
            process_item("slow"),
            timeout=0.5
        )
    except asyncio.TimeoutError:
        print("Timeout!")

    # セマフォで同時実行数制限
    semaphore = asyncio.Semaphore(3)

    async def limited_process(item):
        async with semaphore:
            return await process_item(item)

    results = await asyncio.gather(*[limited_process(i) for i in items])

asyncio.run(main())

4.2 Kotlinコルーチンと構造的(こうぞうてき)同時性(どうじせい)

import kotlinx.coroutines.*

suspend fun fetchUser(id: Int): User {
    delay(1000)
    return User(id, "User $id")
}

fun main() = runBlocking {
    // 構造的同時性: 親キャンセルは子に伝播
    coroutineScope {
        val user = async { fetchUser(1) }
        val orders = async { fetchOrders(1) }

        println("User: ${user.await()}")
        println("Orders: ${orders.await()}")
    } // 全子コルーチン完了まで待機

    // SupervisorScope: 子の失敗が他の子に影響しない
    supervisorScope {
        val job1 = launch {
            delay(100)
            throw RuntimeException("Failed!")
        }
        val job2 = launch {
            delay(200)
            println("Job2 completed") // 正常実行
        }
    }
}

4.3 構造的(こうぞうてき)同時性(どうじせい)の原則(げんそく)

従来の並行性:                   構造的同時性:
┌─────────────┐                ┌─────────────┐
Parent    │                │    Parent│  ┌───┐ ┌───┐│                │  ┌───┐ ┌───┐│
│  │ A │ │ B ││                │  │ A │ │ B ││
│  └─┬─┘ └─┬─┘│                │  └─┬─┘ └─┬─┘│
└────┼─────┼───┘                └────┼─────┼───┘
     │     │ ← 誰が管理?            │     │ ← Parentが管理
     ▼     ▼   リーク可能!          ▼     ▼   自動クリーンアップ!

ルール:
1. すべての並行作業はスコープ内で開始
2. スコープは全子タスク完了まで待機
3. 親キャンセル時に全子もキャンセル
4. 子エラーは親に伝播

5. アクターモデル

5.1 概念(がいねん)

アクターモデルでは各(かく)アクターが:

  • 自分(じぶん)だけの状態(じょうたい)を持(も)つ(他(ほか)のアクターと共有(きょうゆう)しない)
  • メッセージを非同期(ひどうき)で送受信(そうじゅしん)する
  • メッセージ処理時(しょりじ)に新(あたら)しいアクターを作成(さくせい)したり、状態(じょうたい)を変更(へんこう)したり、メッセージを送(おく)ったりできる
┌──────────┐    メッセージ    ┌──────────┐
Actor A │ ───────────────→ │  Actor BState: x │    メッセージ    │ State: y │
Mailbox  │ ←─────────────── │ Mailbox└──────────┘                  └──────────┘
     │ メッセージ
┌──────────┐
Actor CState: z │
Mailbox└──────────┘

核心: 共有状態なし、ロックなし、メッセージ受け渡しのみ

5.2 Erlang/OTP監視(かんし)ツリー

                  ┌──────────────┐
Supervisor                   (one_for_one)                  └──────┬───────┘
                    ┌────┼────┐
                    ▼    ▼    ▼
              ┌─────┐ ┌─────┐ ┌─────┐
W1  │ │ W2  │ │ W3              └─────┘ └─────┘ └─────┘

W2がクラッシュした場合:
1. Supervisorが検知
2. W2のみ再起動(one_for_one戦略)
3. W1W3は影響なし
4. 5分以内に5回以上再起動ならSupervisorも終了

5.3 アクターモデルを使(つか)うべき場合(ばあい)

適している場合:
- 数百万の独立した状態管理(チャットルーム、IoTデバイス)
- 分散システム(ノード間通信)
- 耐障害性が必須のシステム(通信、金融)
- リアルタイムシステム(ゲームサーバー、トレーディング)

適していない場合:
- シンプルなCRUDアプリケーション
- 強い整合性(strong consistency)が必要
- トランザクション処理が核心
- 順序保証が重要なパイプライン

6. 同期(どうき)プリミティブ

6.1 ミューテックス

type SafeMap struct {
    mu   sync.Mutex
    data map[string]int
}

func (m *SafeMap) Set(key string, val int) {
    m.mu.Lock()
    defer m.mu.Unlock()
    m.data[key] = val
}

func (m *SafeMap) Get(key string) (int, bool) {
    m.mu.Lock()
    defer m.mu.Unlock()
    val, ok := m.data[key]
    return val, ok
}

6.2 RWLock(読(よ)み書(か)きロック)

type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

// 複数のゴルーチンが同時に読み取り可能
func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

// 書き込みは排他ロック
func (c *Cache) Set(key, val string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = val
}

6.3 セマフォ

import asyncio

async def worker(name, semaphore):
    async with semaphore:
        print(f"{name} acquired semaphore")
        await asyncio.sleep(1)
        print(f"{name} released semaphore")

async def main():
    semaphore = asyncio.Semaphore(3)  # 最大3つ同時実行
    tasks = [worker(f"Worker-{i}", semaphore) for i in range(10)]
    await asyncio.gather(*tasks)

asyncio.run(main())

6.4 条件変数(じょうけんへんすう)

type BoundedQueue struct {
    mu       sync.Mutex
    notEmpty *sync.Cond
    notFull  *sync.Cond
    items    []interface{}
    maxSize  int
}

func NewBoundedQueue(maxSize int) *BoundedQueue {
    q := &BoundedQueue{maxSize: maxSize}
    q.notEmpty = sync.NewCond(&q.mu)
    q.notFull = sync.NewCond(&q.mu)
    return q
}

func (q *BoundedQueue) Put(item interface{}) {
    q.mu.Lock()
    defer q.mu.Unlock()

    for len(q.items) == q.maxSize {
        q.notFull.Wait() // 空きが出るまで待機
    }
    q.items = append(q.items, item)
    q.notEmpty.Signal() // コンシューマーを起こす
}

func (q *BoundedQueue) Take() interface{} {
    q.mu.Lock()
    defer q.mu.Unlock()

    for len(q.items) == 0 {
        q.notEmpty.Wait() // アイテムが来るまで待機
    }
    item := q.items[0]
    q.items = q.items[1:]
    q.notFull.Signal() // プロデューサーを起こす
    return item
}

7. Lock-Freeプログラミング

7.1 CAS(Compare-And-Swap)

import "sync/atomic"

type LockFreeCounter struct {
    value int64
}

func (c *LockFreeCounter) Increment() int64 {
    for {
        old := atomic.LoadInt64(&c.value)
        new := old + 1
        if atomic.CompareAndSwapInt64(&c.value, old, new) {
            return new
        }
        // CAS失敗、リトライ(他のゴルーチンが先に変更)
    }
}

func (c *LockFreeCounter) Get() int64 {
    return atomic.LoadInt64(&c.value)
}

7.2 ABA問題(もんだい)

スレッドA: 値読み取り → A
                          スレッドB: AB 変更
                          スレッドB: BA 変更(元の値に復元)
スレッドA: CAS(AC) 成功!(しかし途中で値が変わっていた)

解決策:
- バージョン番号追加 (A,v1 → B,v2 → A,v3)
- タグ付きポインタ (JavaのAtomicStampedReference)
- Hazard Pointer

7.3 Lock-Freeを使(つか)うべきでない場合(ばあい)

Lock-Freeが適している場合:
- 非常に高い競合(contention)状況
- リアルタイムシステム(優先度逆転防止)
- 単純なデータ構造(カウンター、スタック、キュー)

Lock-Freeを避けるべき場合(ほとんどの場合):
- 一般的なアプリケーション(Mutexで十分)
- 複雑なデータ構造(ツリー、グラフ)
- 正確性検証が困難な状況
- チームがLock-Freeに慣れていない場合

"Mutexで動くならMutexを使え。"

8. よくある並行性(へいこうせい)バグ

8.1 レースコンディション

// バグ: 複数のゴルーチンが同時にcounterを変更
var counter int

func buggyIncrement() {
    for i := 0; i < 1000; i++ {
        go func() {
            counter++ // 読み取り-変更-書き込みがアトミックでない
        }()
    }
}

// 修正: atomicまたはmutexを使用
var safeCounter int64

func safeIncrement() {
    for i := 0; i < 1000; i++ {
        go func() {
            atomic.AddInt64(&safeCounter, 1)
        }()
    }
}

8.2 デッドロック

4つの必要条件(すべて満たすとデッドロック):
1. 相互排除: リソースを一度に1つだけ使用
2. 保持と待機: リソースを保持しながら別のリソースを待機
3. 非横取り: 保持されたリソースを強制的に奪えない
4. 循環待機: ABCA 循環待機
// デッドロック例
var mu1, mu2 sync.Mutex

func goroutine1() {
    mu1.Lock()
    defer mu1.Unlock()
    time.Sleep(time.Millisecond)
    mu2.Lock()       // mu2待機(goroutine2が保持)
    defer mu2.Unlock()
}

func goroutine2() {
    mu2.Lock()
    defer mu2.Unlock()
    time.Sleep(time.Millisecond)
    mu1.Lock()       // mu1待機(goroutine1が保持)
    defer mu1.Unlock()
}

// 修正: 常に同じ順序でロック取得
func fixed1() {
    mu1.Lock()
    defer mu1.Unlock()
    mu2.Lock()
    defer mu2.Unlock()
}

func fixed2() {
    mu1.Lock()       // mu1を先に(同じ順序)
    defer mu1.Unlock()
    mu2.Lock()
    defer mu2.Unlock()
}

8.3 ライブロック

ABが狭い廊下で出会う:
A: 左に避ける ← → B: 右に避ける(同じ方向!A: 右に避ける ← → B: 左に避ける(また同じ方向!...(無限ループ、誰も進めない)

vs デッドロック: 何もしない
vs ライブロック: 動き続けるが進展なし

8.4 飢餓(きが)(Starvation)

優先度(ゆうせんど)の高(たか)いスレッドがリソースを占有(せんゆう)し続(つづ)け、低(ひく)い優先度(ゆうせんど)のスレッドが永遠(えいえん)に実行(じっこう)できない状態(じょうたい)。

8.5 優先度逆転(ゆうせんどぎゃくてん)

1997年火星探査機Mars Pathfinder事件:

Low-priorityタスクがミューテックスを保持
   |
Medium-priorityタスクがLowを横取り(ミューテックスと無関係)
   |
High-priorityタスクがミューテックス待ち... 永遠に!
(Lowが実行されないとミューテックスが解放されないが、Mediumが常に横取り)

解決策: 優先度継承プロトコル
Lowがミューテックス保持中はHighの優先度を一時的に継承

8.6 検出(けんしゅつ)ツール

# Go: レースディテクター
go test -race ./...
go run -race main.go

# Java: Thread Sanitizer
java -XX:+UseThreadSanitizer MyApp

# Rust: コンパイラがコンパイル時に検出!
# Send/Syncトレイト違反 = コンパイルエラー

# Python: asyncioデバッグモード
PYTHONASYNCIODEBUG=1 python app.py

# 一般: Helgrind (Valgrindツール)
valgrind --tool=helgrind ./program

9. 実践(じっせん)パターン

9.1 プロデューサー・コンシューマー

func producerConsumer() {
    ch := make(chan int, 100)

    var prodWg sync.WaitGroup
    for i := 0; i < 3; i++ {
        prodWg.Add(1)
        go func(id int) {
            defer prodWg.Done()
            for j := 0; j < 10; j++ {
                ch <- id*100 + j
            }
        }(i)
    }

    go func() {
        prodWg.Wait()
        close(ch)
    }()

    var consWg sync.WaitGroup
    for i := 0; i < 2; i++ {
        consWg.Add(1)
        go func(id int) {
            defer consWg.Done()
            for item := range ch {
                fmt.Printf("Consumer %d: %d\n", id, item)
            }
        }(i)
    }

    consWg.Wait()
}

9.2 ワークスティーリング

従来のスレッドプール:          ワークスティーリング:
┌─────────────┐              ┌─────────────┐
│ 共有キュー   │              │ Worker 1[T1|T2|T3]  │              │ ローカル:[T1]│             │              ├─────────────┤
│ 全ワーカーが │              │ Worker 2│ 競合!       │              │ ローカル:[T2]│ ← 空ならW1から窃取
│             │              ├─────────────┤
└─────────────┘              │ Worker 3                             │ ローカル:[T3]                             └─────────────┘

利点: キャッシュ局所性向上、競合削減
使用: Java ForkJoinPool、Goランタイム、Tokio(Rust)

10. パフォーマンス

10.1 アムダールの法則(ほうそく)

高速化 = 1 / ((1 - P) + P/N)

P = 並列化可能な割合
N = プロセッサ数

: 95%並列化可能、64コアの場合
高速化 = 1 / (0.05 + 0.95/64) = 1 / 0.0648 = 15.4
核心: コアをいくら増やしても直列部分(5%)がボトルネック!
最大理論的高速化 = 1/0.05 = 20

10.2 グスタフソンの法則(ほうそく)

高速化 = N - alpha * (N - 1)

alpha = 直列部分の割合
N = プロセッサ数

核心的な違い:
- アムダール: 「問題サイズ固定、コアを増やすと?- グスタフソン: 「コア固定、問題サイズを増やすと?
現実ではデータが増えると並列部分も増える
→ グスタフソンがより楽観的で現実的

10.3 コンテキストスイッチコスト

コンテキストスイッチコスト比較:
┌───────────────────────────┬──────────────┐
│ スイッチ種類              │ おおよそ     │
├───────────────────────────┼──────────────┤
│ 関数呼び出し              │ ~1-5 ns      │
│ ゴルーチンスイッチ        │ ~100-200 ns  │
コルーチンスイッチ(Python)~1-5 us      │
スレッドスイッチ(同プロセス)~1-10 us   │
│ プロセススイッチ          │ ~10-50 us    │
│ 仮想マシンスイッチ        │ ~100+ us     │
└───────────────────────────┴──────────────┘

10.4 False Sharing(キャッシュライン問題(もんだい))

問題: 異なる変数が同じキャッシュラインに位置

CPU Core 1          CPU Core 2
    |                   |
+---v-------------------v---+
| Cache Line (64 bytes)     |
| [counter_A] [counter_B]   |  ← 異なる変数だが
|                           |     同じキャッシュライン!
+---------------------------+

Core 1がcounter_Aを変更 → キャッシュライン無効化 → Core 2もリロード必要
Core 2がcounter_Bを変更 → キャッシュライン無効化 → Core 1もリロード必要

解決: パディングでキャッシュラインを分離
type PaddedCounter struct {
    value int64
    _pad  [56]byte // 64バイトキャッシュラインパディング
}

type Counters struct {
    c1 PaddedCounter
    c2 PaddedCounter
}

11. 面接(めんせつ)質問(しつもん)15選(せん)

  1. 並行性(へいこうせい)と並列性(へいれつせい)の違(ちが)いは? 並行性(へいこうせい)は複数(ふくすう)のタスクを交互(こうご)に処理(しょり)する構造(こうぞう)、並列性(へいれつせい)は物理的(ぶつりてき)に同時(どうじ)に実行(じっこう)すること。シングルコアでも並行性(へいこうせい)は可能(かのう)だが並列性(へいれつせい)は不可(ふか)。

  2. プロセスとスレッドの違(ちが)いは? プロセスは独立(どくりつ)したメモリ空間(くうかん)、スレッドはプロセス内(ない)でメモリ共有(きょうゆう)。スレッドの方(ほう)が生成(せいせい)/切替(きりかえ)コストが低(ひく)いが同期(どうき)が必要(ひつよう)。

  3. デッドロックの4つの必要条件(ひつようじょうけん)と解決法(かいけつほう)は? 相互排除(そうごはいじょ)、保持(ほじ)と待機(たいき)、非横取(よこど)り、循環待機(じゅんかんたいき)。順序(じゅんじょ)付(つ)きロック取得(しゅとく)(循環待機防止(じゅんかんたいきぼうし))が最(もっと)も実用的(じつようてき)。

  4. Goゴルーチンがosスレッドより軽(かる)い理由(りゆう)は? 約(やく)2KBスタック(OSスレッドは1MB+)、M:Nスケジューラで少数(しょうすう)のOSスレッドに数百万(すうひゃくまん)のゴルーチンをマッピング。

  5. PythonのGILとは何(なに)か、なぜ存在(そんざい)するか? Global Interpreter Lock。CPythonの参照(さんしょう)カウントメモリ管理(かんり)がスレッドセーフでないため必要(ひつよう)。CPUバウンドにはmultiprocessingを使用(しよう)。

  6. イベントループとマルチスレッディングの長所(ちょうしょ)と短所(たんしょ)は? イベントループ:シンプルなモデル、コンテキストスイッチなし、CPUバウンドに不向(ふむ)き。マルチスレッド:CPU活用(かつよう)最大化(さいだいか)、同期(どうき)が複雑(ふくざつ)、デッドロックリスク。

  7. async/awaitがスレッドと異(こと)なる点(てん)は? async/awaitは協調的(きょうちょうてき)マルチタスキングで、awaitポイントでのみ切替(きりか)え。スレッドはプリエンプティブでOSがいつでも切替(きりか)え可能(かのう)。

  8. アクターモデルの長所(ちょうしょ)と短所(たんしょ)は? 長所(ちょうしょ):共有状態(きょうゆうじょうたい)なし、デッドロックなし、分散(ぶんさん)システムに自然(しぜん)。短所(たんしょ):メッセージ順序保証(じゅんじょほしょう)が困難(こんなん)、デバッグが複雑(ふくざつ)。

  9. CAS演算(えんざん)とは?いつMutexの代(か)わりに使(つか)うか? Compare-And-Swapはアトミックな比較交換(ひかくこうかん)演算(えんざん)。単純(たんじゅん)なカウンターやポインタ更新(こうしん)ではMutexより高速(こうそく)だが、複雑(ふくざつ)なロジックには不向(ふむ)き。

  10. False Sharingとは?解決方法(かいけつほうほう)は? 異(こと)なるコアが同(おな)じキャッシュラインの異(こと)なる変数(へんすう)を変更(へんこう)する際(さい)に発生(はっせい)するパフォーマンス低下(ていか)。パディングで変数(へんすう)を別(べつ)のキャッシュラインに配置(はいち)して解決(かいけつ)。

  11. アムダールの法則(ほうそく)が示唆(しさ)することは? 直列部分(ちょくれつぶぶん)が全体(ぜんたい)の速度向上(そくどこうじょう)の上限(じょうげん)を決定(けってい)。並列化(へいれつか)の最適化(さいてきか)より直列部分(ちょくれつぶぶん)の縮小(しゅくしょう)がより重要(じゅうよう)な場合(ばあい)がある。

  12. Java Virtual ThreadとGoゴルーチンの違(ちが)いは? どちらもM:Nモデルだが、Goはチャネルベースのcsp、Javaは既存(きそん)のThread API互換性(ごかんせい)を維持(いじ)。Goが先(さき)に導入(どうにゅう)、Java 21で正式(せいしき)サポート。

  13. 構造的同時性(こうぞうてきどうじせい)(Structured Concurrency)とは? 並行(へいこう)タスクをスコープに結(むす)びつけてライフサイクルを管理(かんり)。親(おや)キャンセルは子(こ)に伝播(でんぱ)、親(おや)は全(すべ)ての子(こ)の完了(かんりょう)を待機(たいき)。ゴルーチン/スレッドリークを防止(ぼうし)。

  14. Erlangの「Let it crash」哲学(てつがく)とは? エラーを防御的(ぼうぎょてき)に処理(しょり)するより、プロセスを素早(すばや)く終了(しゅうりょう)してスーパーバイザーに再起動(さいきどう)させる方(ほう)が安全(あんぜん)。複雑(ふくざつ)なエラー回復(かいふく)コードが不要(ふよう)に。

  15. スレッドプールの適切(てきせつ)なサイズは? CPUバウンド:CPUコア数(すう)。I/Oバウンド:CPUコア数(すう) * (1 + 待機時間(たいきじかん)/処理時間(しょりじかん))。Littleの法則(ほうそく)(L = lambda * W)で推定可能(すいていかのう)。


12. 実践(じっせん)クイズ

Q1: このJavaScriptコードの出力(しゅつりょく)順序(じゅんじょ)は?
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');

答(こた)え: A, D, C, B。同期(どうき)コード(A, D)が最初(さいしょ)→マイクロタスク(Promise: C)→マクロタスク(setTimeout: B)。

Q2: Goでこのコードがデッドロックになる理由(りゆう)は?
func main() {
    ch := make(chan int)
    ch <- 42
    fmt.Println(<-ch)
}

答(こた)え: アンバッファチャネルは送信者(そうしんしゃ)と受信者(じゅしんしゃ)が同時(どうじ)に存在(そんざい)する必要(ひつよう)があります。メインゴルーチンがch <- 42で受信者(じゅしんしゃ)を待(ま)ってブロックされ、<-chに到達(とうたつ)できません。

Q3: Pythonでasyncio.gatherとasyncio.waitの違(ちが)いは?

答(こた)え: gatherはすべての結果(けっか)を順番(じゅんばん)に返(かえ)し、1つでも失敗(しっぱい)すると例外(れいがい)を発生(はっせい)させます(return_exceptions=Trueで変更可能(へんこうかのう))。waitは完了(かんりょう)/未完了(みかんりょう)のセットを返(かえ)し、FIRST_COMPLETED、FIRST_EXCEPTION、ALL_COMPLETEDオプションを提供(ていきょう)します。waitの方(ほう)が柔軟(じゅうなん)です。

Q4: アムダールの法則(ほうそく)によると、90%を並列化(へいれつか)できるプログラムの最大(さいだい)高速化(こうそくか)は?

答(こた)え: コアを無限(むげん)に増(ふ)やしても最大(さいだい)高速化(こうそくか) = 1/(1-0.9) = 1/0.1 = **10倍(ばい)**です。10%の直列部分(ちょくれつぶぶん)がボトルネックになります。64コアでは1/(0.1 + 0.9/64) = 約(やく)8.8倍(ばい)です。

Q5: Rustがコンパイル時(じ)にデータレースを防止(ぼうし)する原理(げんり)は?

答(こた)え: Rustの所有権(しょゆうけん)システムとSend/Syncトレイトが核心(かくしん)です。Sendはスレッド間(かん)の所有権(しょゆうけん)移転(いてん)が可能(かのう)、Syncはスレッド間(かん)の参照共有(さんしょうきょうゆう)が可能(かのう)を意味(いみ)します。可変参照(かへんさんしょう)は同時(どうじ)に1つしか存在(そんざい)できないため(借用(しゃくよう)チェッカー)、コンパイル時(じ)にデータレースを完全(かんぜん)に防止(ぼうし)します。


参考資料(さんこうしりょう)

  1. Rob Pike - Concurrency Is Not Parallelism (Talk)
  2. The Art of Multiprocessor Programming (Herlihy & Shavit)
  3. Java Concurrency in Practice (Goetz et al.)
  4. JEP 444: Virtual Threads
  5. Node.js Event Loop Documentation
  6. Python asyncio Documentation
  7. Tokio Tutorial (Rust)
  8. Erlang/OTP Design Principles
  9. Go Concurrency Patterns
  10. Kotlin Coroutines Guide
  11. Lock-Free Programming (Preshing)
  12. Amdahl's Law (Wikipedia)
  13. LMAX Disruptor (Lock-Free Queue)
  14. Structured Concurrency (Wikipedia)
  15. Thread Sanitizer (Google)