プログラミングにおける並列処理の競合
#並行プログラミング
1. 命令レベルの競合
Race Condition(競合状態)
複数のスレッドが共有データに同時にアクセスし、実行順序によって結果が非決定的に変わる問題。多くの競合問題の根本原因となります。
code: (ruby)
# 危険なコード例
@counter = 0
threads = []
threads << Thread.new { 100.times { @counter += 1 } }
threads << Thread.new { 100.times { @counter += 1 } }
threads.each(&:join)
puts @counter # 期待値: 200、実際: 147など(実行ごとに異なる)
Check-Then-Act パターン
共有データに対する「チェック(条件判断)」と「アクション(実行)」の間に、別のスレッドが割り込むことで問題が発生するパターン。
code: (ruby)
# 危険なコード例:残高引き出し
if @balance >= amount # Check: 残高が十分か確認
# ここで他のスレッドが残高を引き出すなどして、@balanceが不足する可能性
@balance -= amount # Act: 残高を減らす
return true
end
Read-Modify-Write コンフリクト
共有変数に対する「読み取り → 変更 → 書き込み」の一連の操作が原子的でないために発生する競合。
code: (ruby)
# 危険なコード例
value = shared_variable # Read: shared_variableの値を読み取る
value = value * 2 # Modify: 読み取った値を変更する
# ここで他のスレッドがshared_variableを更新すると、その更新が失われる可能性
shared_variable = value # Write: 新しい値を書き込む
Lost Update(ロストアップデート)
Read-Modify-Writeコンフリクトの結果として発生する、複数のスレッドの更新が失われる問題。
2. リソース管理レベルの競合
Deadlock(デッドロック)
複数のスレッドが、互いが保持しているリソースの解放を待ち合い、永久に停止してしまう状態。
code: (ruby)
# デッドロックの例
mutex_a = Mutex.new
mutex_b = Mutex.new
thread1 = Thread.new do
mutex_a.lock
puts "Thread 1: mutex_a をロックしました"
sleep 0.1
mutex_b.lock # Thread 2がmutex_bを持っているので待機
puts "Thread 1: mutex_b もロックしました" # ここには到達しません
end
thread2 = Thread.new do
mutex_b.lock
puts "Thread 2: mutex_b をロックしました"
sleep 0.1
mutex_a.lock # Thread 1がmutex_aを持っているので待機
puts "Thread 2: mutex_a もロックしました" # ここには到達しません
end
デッドロック回避策:
ロックの順序を統一する
タイムアウトを設定する
デッドロック検出機構を使用する
Livelock(ライブロック)
デッドロックを避けようとして、スレッド同士が互いにリソースを譲り合い続け、結果的にどのスレッドも処理を進められない状態。
Starvation(スターベーション)
特定のスレッドが、他のスレッドに優先されるなどして、共有リソースへのアクセスを永続的に拒否される状態。
3. 対策手法
排他制御(Mutex, Lock)
特定のコードブロックやデータへのアクセスを一度に一つのスレッドのみに制限。
code: (ruby)
# Mutex使用例
@mutex = Mutex.new
@shared_data = 0
def increment
@mutex.synchronize do
@shared_data += 1
end
end
特徴:
確実な排他制御が可能
デッドロックのリスクあり
パフォーマンスのボトルネックになる可能性
原子操作(CAS, AtomicInteger)
ハードウェアレベルで不可分に実行される操作。Compare-And-Swap(CAS)が代表的。
code: (ruby)
# Rubyでの原子操作例(concurrent-ruby gem使用)
require 'concurrent'
counter = Concurrent::AtomicFixnum.new(0)
counter.increment # 原子的にインクリメント
利点:
ロックフリーで高性能
デッドロックが発生しない
スケーラビリティが高い
ロックフリーデータ構造
ロックを使用せずに並列アクセスを安全に処理できるデータ構造。
code: (ruby)
# concurrent-ruby gemのロックフリーデータ構造例
require 'concurrent'
# ロックフリーキュー
queue = Concurrent::LockFreeQueue.new
queue.push(item)
item = queue.pop
イミュータブル設計
一度作成されたらその状態を変更できないオブジェクトを使用。
code: (ruby)
# イミュータブルなクラス設計例
class ImmutablePoint
attr_reader :x, :y
def initialize(x, y)
@x = x.freeze
@y = y.freeze
freeze
end
def move(dx, dy)
ImmutablePoint.new(@x + dx, @y + dy)
end
end
利点:
競合状態が発生しない
スレッドセーフ
副作用がなく理解しやすい
スレッドローカル記憶域
各スレッドが独立したデータを持つための領域。
code: (ruby)
# Thread-local変数の使用例
Thread.current:user_id = 123
def get_current_user_id
Thread.current:user_id
end
アクターモデル
各アクターが独立したオブジェクトとして振る舞い、メッセージパッシングによってのみ通信を行う並行処理モデル。
code: (ruby)
# concurrent-ruby のActorモデル例
require 'concurrent'
class CounterActor < Concurrent::Actor::RestartingContext
def initialize
@count = 0
end
def on_message(message)
case message
when :increment
@count += 1
when :get
@count
end
end
end
counter = CounterActor.spawn(:counter)
counter << :increment
result = counter.ask!(:get)