- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに:並行性(へいこうせい)はなぜ難(むずか)しいのか?
- 1. スレッディングモデル
- 2. 言語別(げんごべつ)並行性(へいこうせい)比較(ひかく)
- 3. イベントループ深掘(ふかぼ)り
- 4. コルーチンと構造的(こうぞうてき)同時性(どうじせい)
- 5. アクターモデル
- 6. 同期(どうき)プリミティブ
- 7. Lock-Freeプログラミング
- 8. よくある並行性(へいこうせい)バグ
- 9. 実践(じっせん)パターン
- 10. パフォーマンス
- 11. 面接(めんせつ)質問(しつもん)15選(せん)
- 12. 実践(じっせん)クイズ
- 参考資料(さんこうしりょう)
はじめに:並行性(へいこうせい)はなぜ難(むずか)しいのか?
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:1 │ N:1 │ M:N │
├─────────────────┼──────────────┼──────────────┼──────────────┤
│ スレッドコスト │ ~1MBスタック │ ~KBスタック │ ~KBスタック │
│ 生成速度 │ 遅い(~ms) │ 速い(~us) │ 速い(~us) │
│ コンテキスト │ 高い(カーネル)│ 低い(ユーザー)│ 低い(ユーザー)│
│ マルチコア │ O │ X │ O │
│ I/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 言語別(げんごべつ)比較表(ひかくひょう)
┌─────────────┬─────────────┬───────────────┬──────────────┬──────────────┐
│ 言語 │ モデル │ 単位 │ スケジュール │ 特徴 │
├─────────────┼─────────────┼───────────────┼──────────────┼──────────────┤
│ Go │ CSP (M:N) │ ゴルーチン │ ランタイム │ チャネル通信 │
│ Java 21+ │ M:N │ Virtual Thread│ JVM │ 後方互換 │
│ Python │ イベントルー│ コルーチン │ asyncio │ GIL制約 │
│ Rust │ async/await │ Future/Task │ tokio等 │ ゼロコスト │
│ JavaScript │ イベントルー│ Promise │ libuv/V8 │ シングル │
│ Erlang │ Actor(M:N) │ プロセス │ BEAM VM │ 耐障害性 │
│ Kotlin │ コルーチン │ Coroutine │ Dispatcher │ 構造的同時性 │
└─────────────┴─────────────┴───────────────┴──────────────┴──────────────┘
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/O │
│ DNS 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 B │
│ State: x │ メッセージ │ State: y │
│ Mailbox │ ←─────────────── │ Mailbox │
└──────────┘ └──────────┘
│
│ メッセージ
▼
┌──────────┐
│ Actor C │
│ State: z │
│ Mailbox │
└──────────┘
核心: 共有状態なし、ロックなし、メッセージ受け渡しのみ
5.2 Erlang/OTP監視(かんし)ツリー
┌──────────────┐
│ Supervisor │
│ (one_for_one)│
└──────┬───────┘
┌────┼────┐
▼ ▼ ▼
┌─────┐ ┌─────┐ ┌─────┐
│ W1 │ │ W2 │ │ W3 │
└─────┘ └─────┘ └─────┘
W2がクラッシュした場合:
1. Supervisorが検知
2. W2のみ再起動(one_for_one戦略)
3. W1、W3は影響なし
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: A → B 変更
スレッドB: B → A 変更(元の値に復元)
スレッドA: CAS(A → C) 成功!(しかし途中で値が変わっていた)
解決策:
- バージョン番号追加 (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. 循環待機: A→B→C→A 循環待機
// デッドロック例
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 ライブロック
AとBが狭い廊下で出会う:
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選(せん)
-
並行性(へいこうせい)と並列性(へいれつせい)の違(ちが)いは? 並行性(へいこうせい)は複数(ふくすう)のタスクを交互(こうご)に処理(しょり)する構造(こうぞう)、並列性(へいれつせい)は物理的(ぶつりてき)に同時(どうじ)に実行(じっこう)すること。シングルコアでも並行性(へいこうせい)は可能(かのう)だが並列性(へいれつせい)は不可(ふか)。
-
プロセスとスレッドの違(ちが)いは? プロセスは独立(どくりつ)したメモリ空間(くうかん)、スレッドはプロセス内(ない)でメモリ共有(きょうゆう)。スレッドの方(ほう)が生成(せいせい)/切替(きりかえ)コストが低(ひく)いが同期(どうき)が必要(ひつよう)。
-
デッドロックの4つの必要条件(ひつようじょうけん)と解決法(かいけつほう)は? 相互排除(そうごはいじょ)、保持(ほじ)と待機(たいき)、非横取(よこど)り、循環待機(じゅんかんたいき)。順序(じゅんじょ)付(つ)きロック取得(しゅとく)(循環待機防止(じゅんかんたいきぼうし))が最(もっと)も実用的(じつようてき)。
-
Goゴルーチンがosスレッドより軽(かる)い理由(りゆう)は? 約(やく)2KBスタック(OSスレッドは1MB+)、M:Nスケジューラで少数(しょうすう)のOSスレッドに数百万(すうひゃくまん)のゴルーチンをマッピング。
-
PythonのGILとは何(なに)か、なぜ存在(そんざい)するか? Global Interpreter Lock。CPythonの参照(さんしょう)カウントメモリ管理(かんり)がスレッドセーフでないため必要(ひつよう)。CPUバウンドにはmultiprocessingを使用(しよう)。
-
イベントループとマルチスレッディングの長所(ちょうしょ)と短所(たんしょ)は? イベントループ:シンプルなモデル、コンテキストスイッチなし、CPUバウンドに不向(ふむ)き。マルチスレッド:CPU活用(かつよう)最大化(さいだいか)、同期(どうき)が複雑(ふくざつ)、デッドロックリスク。
-
async/awaitがスレッドと異(こと)なる点(てん)は? async/awaitは協調的(きょうちょうてき)マルチタスキングで、awaitポイントでのみ切替(きりか)え。スレッドはプリエンプティブでOSがいつでも切替(きりか)え可能(かのう)。
-
アクターモデルの長所(ちょうしょ)と短所(たんしょ)は? 長所(ちょうしょ):共有状態(きょうゆうじょうたい)なし、デッドロックなし、分散(ぶんさん)システムに自然(しぜん)。短所(たんしょ):メッセージ順序保証(じゅんじょほしょう)が困難(こんなん)、デバッグが複雑(ふくざつ)。
-
CAS演算(えんざん)とは?いつMutexの代(か)わりに使(つか)うか? Compare-And-Swapはアトミックな比較交換(ひかくこうかん)演算(えんざん)。単純(たんじゅん)なカウンターやポインタ更新(こうしん)ではMutexより高速(こうそく)だが、複雑(ふくざつ)なロジックには不向(ふむ)き。
-
False Sharingとは?解決方法(かいけつほうほう)は? 異(こと)なるコアが同(おな)じキャッシュラインの異(こと)なる変数(へんすう)を変更(へんこう)する際(さい)に発生(はっせい)するパフォーマンス低下(ていか)。パディングで変数(へんすう)を別(べつ)のキャッシュラインに配置(はいち)して解決(かいけつ)。
-
アムダールの法則(ほうそく)が示唆(しさ)することは? 直列部分(ちょくれつぶぶん)が全体(ぜんたい)の速度向上(そくどこうじょう)の上限(じょうげん)を決定(けってい)。並列化(へいれつか)の最適化(さいてきか)より直列部分(ちょくれつぶぶん)の縮小(しゅくしょう)がより重要(じゅうよう)な場合(ばあい)がある。
-
Java Virtual ThreadとGoゴルーチンの違(ちが)いは? どちらもM:Nモデルだが、Goはチャネルベースのcsp、Javaは既存(きそん)のThread API互換性(ごかんせい)を維持(いじ)。Goが先(さき)に導入(どうにゅう)、Java 21で正式(せいしき)サポート。
-
構造的同時性(こうぞうてきどうじせい)(Structured Concurrency)とは? 並行(へいこう)タスクをスコープに結(むす)びつけてライフサイクルを管理(かんり)。親(おや)キャンセルは子(こ)に伝播(でんぱ)、親(おや)は全(すべ)ての子(こ)の完了(かんりょう)を待機(たいき)。ゴルーチン/スレッドリークを防止(ぼうし)。
-
Erlangの「Let it crash」哲学(てつがく)とは? エラーを防御的(ぼうぎょてき)に処理(しょり)するより、プロセスを素早(すばや)く終了(しゅうりょう)してスーパーバイザーに再起動(さいきどう)させる方(ほう)が安全(あんぜん)。複雑(ふくざつ)なエラー回復(かいふく)コードが不要(ふよう)に。
-
スレッドプールの適切(てきせつ)なサイズは? 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つしか存在(そんざい)できないため(借用(しゃくよう)チェッカー)、コンパイル時(じ)にデータレースを完全(かんぜん)に防止(ぼうし)します。
参考資料(さんこうしりょう)
- Rob Pike - Concurrency Is Not Parallelism (Talk)
- The Art of Multiprocessor Programming (Herlihy & Shavit)
- Java Concurrency in Practice (Goetz et al.)
- JEP 444: Virtual Threads
- Node.js Event Loop Documentation
- Python asyncio Documentation
- Tokio Tutorial (Rust)
- Erlang/OTP Design Principles
- Go Concurrency Patterns
- Kotlin Coroutines Guide
- Lock-Free Programming (Preshing)
- Amdahl's Law (Wikipedia)
- LMAX Disruptor (Lock-Free Queue)
- Structured Concurrency (Wikipedia)
- Thread Sanitizer (Google)