Slide 1

Slide 1 text

Java スレッドプログラミングの基礎 かとじゅん(@j5ik2o)

Slide 2

Slide 2 text

ムーアの法則とは 2

Slide 3

Slide 3 text

「半導体の集積密度は18 ~24 カ月で倍増し、チップは処理能力 が倍になってもさらに小型化が進むという法則( 経験則) 」。 1965 年に、インテル社創設者の1 人であるゴードンムーアが提 唱 つまり、1 年半から2 年でIC チップに集積されるトランジスタ 数が倍増していく( プロセスの微細化とも呼ばれる) ということ 微細化が滞るとコンピューティングの進化も止まってしま う、と考えられていた 3

Slide 4

Slide 4 text

ムーアの法則を維持する戦略 クロック周波数をあげる トランジスタの集積度をあげ、1 秒間で実行できる命令数を増 やす戦略。クロックとはCPU の動作基準となる時間の単位。 3GHz は1 秒間で30 億回のクロックを表す ハードウェアを改良すればソフトウェアのパフォーマンスも 改善された時代 CPU に搭載するコアを複数にする クロック数アップ戦略は発熱やエネルギー効率の問題が障害 となり、現在では複数のタスクをそれぞれのコアが並列 (Parallel) に処理できる、マルチコアが一般的になっている 4

Slide 5

Slide 5 text

FYI: 並行処理と並列処理 並行(Concurrent) は、「複数の動作が、論理的に、順不同もし くは同時に起こりうる」こと 1CPU で複数の仕事を並行処理するには、処理時間を非常に短 い時間単位で分割する、タイムスライス( タイムクォンタム) が 利用される。ミクロな観点では同時ではない 並列(Parallel) は、「複数の動作が、物理的に同時に起こるこ と」 - 目的は計算速度の向上。スレッドをコアに割り当てて、並列 にタスクを処理させる必要がある ※本スライドでは、この二つの概念を特に区別しない場合は、 並 行 を一般的な概念を示す用語として利用する 5

Slide 6

Slide 6 text

マルチスレッドは難しい? スレッドAPI を使えば並行処理は実現可能だが、陥りやすい代 表的な問題について考えてみよう タスク量に比例してスレッド数が増加する問題 処理中のタスクを安全に中断できない問題 競り合い状態や命令の順序替えによって想定外の挙動をする 問題 6

Slide 7

Slide 7 text

scala.concurrent.Future, akka-actor を有用 性を語る前に java.lang.Thread について知 ろう 7

Slide 8

Slide 8 text

スレッド数が増加する問題 8

Slide 9

Slide 9 text

タスクの要求毎にスレッドを生成する例 class CommandExecutor { def execute(cmd: () => Unit): Unit = { /* タスクが大量に同時発生した場合、スレッドも大量に生成され システムリソースがいたずらに消費されてしまう */ new Thread(() => cmd()).start() } } OS ごとに作成できるスレッド数の上限は決まっている。その上 限を超えるとOutOfMemoryError 生成後は、アクティブかどうかに関わらずメモリを消費する 9

Slide 10

Slide 10 text

ExecutorService( スレッドプール) を利用す る class CommandExecutor(nThreads: Int) { // 固定長のスレッドプールを持つExecutorService private val pool: ExecutorService = Executors .newFixedThreadPool(nThreads) def execute(cmd: () => Unit): Future[_] = { // タスクをスレッドプールにサブミットする pool.submit(() => cmd()) } } 10

Slide 11

Slide 11 text

スレッドプールのイメージ Google 画像検索 11

Slide 12

Slide 12 text

Executors のファクトリ newFixedThreadPool 固定サイズのスレッドプールを作る newCachedThreadPool サイズ制限なしのスレッドプール。スレッド数は要求によっ て増減 newSingleThreadExecutor 1 つのワーカースレッドのみ。逐次処理向き newScheduledThreadPool タスクの遅延開始と周期的実行をサポート 12

Slide 13

Slide 13 text

FYI: 計算結果を返すCallable case class Result(in: Long, out: BigDecimal) class Fac(i: Long) extends Callable[Result] { private def fac(n: Long): BigDecimal = (1L to n).foldLeft(BigDecimal(1L))(_ * _) override def call(): Result = Result(i, fac(i)) } 計算結果を返す場合はRunnable ではなくFuture(Java) を使う 13

Slide 14

Slide 14 text

val executor = Executors.newFixedThreadPool(3) val futures = ArrayBuffer[Future[Result]]() for (i <- 2L to 50L) { val future: Future[Result] = executor.submit(new Fac(i)) futures.append(future) } futures.foreach { future => try { val result = future.get() // 値が取得できるまでブロックする println(f"fac(${result.in}%d) = ${result.out}%s") } catch { case ex: Throwable => ex.printStackTrace() } } executor.shutdown() executor.awaitTermination(5, TimeUnit.SECONDS) 14

Slide 15

Slide 15 text

タスクを安全に中断できない問題 15

Slide 16

Slide 16 text

実行中のスレッドを安全に停止するAPI は提供されていない (Thread.stop, Thread.suspend は非推奨/ 欠陥がある) 安全に停止させるには、スレッドにメッセージを送り、タスク を協力的に中断する仕組みが必要 主に変数の可視性とブロッキングの問題が関係する 16

Slide 17

Slide 17 text

メモリモデル: 可視性の問題 スレッドt は停止しません。なぜかわかりますか?(HotSpot Server VM) private var stopRequested = false val t = new Thread(() => { println("thread: start") var i = 0 while (!stopRequested) { i += 1 } println("thread: finish") }) t.start() TimeUnit.SECONDS.sleep(1) println("stop request") stopRequested = true 17

Slide 18

Slide 18 text

以下のコードに変換されてしまう( 巻き上げ最適化) if (!stopRequested) while(true) { i += 1 } Java メモリモデルでは、スレッドごとに最適化するため、値の 変更が他のスレッドへ即座に伝わらない可能性がある デフォルトではスレッドからの書き込みが、別のスレッドが読 み込める保証( 可視性) がない。@volatile を使えば、異なるスレ ッドからも読めるようになる @volatile private var stopRequested = false // 巻き上げ最適化はされない 18

Slide 19

Slide 19 text

FYI: volatile の有効範囲 case class Persion(name: String) @volatile val person = Person("KATO") @volatile val persons = Array(Person("KATO")) // 指定された参照以外は可視性は保証されません val name = person.name val p = persons(0) 19

Slide 20

Slide 20 text

固有ロック(synchronized) を使う方法 private var stopRequested = false def requestStop(): Unit = synchronized { stopRequested = true } def isStopRequested(): Boolean = synchronized { stopRequested } val t = new Thread(() => { println("thread: start") var i = 0 while (!isStopRequested) { i += 1 } println("thread: finish") }) t.start() TimeUnit.SECONDS.sleep(1) println("stop request") requestStop() 20

Slide 21

Slide 21 text

固有ロック(synchronized) 固有ロック(synchronized) には、アトミック性を保証する以外 に、可視性を保証する機能もある 事前発生(happens-before) の原理で可視性を保証 あるスレッドが取得したロック内で行ったすべての操作は、 異なるスレッドがロックを取得するとき、可視となる。 参照: 「Java 言語仕様3 版」の「17.4.5 先行発生の順序」 21

Slide 22

Slide 22 text

ブロッキング中にスレッドを停止する例 フィボナッチ数列をブロッキングキューに追加するタスクを停止 させる object Main extends App { val task = new FibGenerator() val thread = new Thread(task) println("thread start") thread.start() TimeUnit.SECONDS.sleep(1) println("cancel start") task.cancel() thread.join() println("cancel finish") println(s"result = ${task.fibonaccis().toList}") } 22

Slide 23

Slide 23 text

// フィボナッチ数列をブロッキングキューに追加するタスク class FibGenerator extends Runnable { @volatile private var cancelled = false @volatile private var thread: Thread = _ private val queue = new ArrayBlockingQueue[BigInt](2) def fibonaccis(): Iterable[BigInt] = queue.asScala // ... 23

Slide 24

Slide 24 text

ブロック中はフラグを変えても終了できない。この例ではタスク を実行するスレッドに対して、インタラプションを発生させてタ スクを中断させている。 override def run(): Unit = { thread = Thread.currentThread() var n = BigInt(1) try { while (!cancelled) { val f = fibonacci(n) println(f"fib($n) = $f") queue.put(f) // キューがいっぱいになるとブロックする n = n + BigInt(1) } } catch { case _: InterruptedException => println(" 割り込みが発生しました") } } 24

Slide 25

Slide 25 text

def cancel(): Unit = { require(thread != null) cancelled = true thread.interrupt() // インタラプションを発生 } private def fibonacci(n: BigInt): BigInt = { if (n == BigInt(0) || n == BigInt(1)) n else { val a = n + BigInt(-2) val b = n + BigInt(-1) fibonacci(a) + fibonacci(b) } } } BlockingQueue#put はインタラプション発生時にブロックを解 除してくれるが、実装依存。一般的には実装されるべき。 25

Slide 26

Slide 26 text

想定外の挙動をする問題 26

Slide 27

Slide 27 text

競り合い状態 class Sequence(private var value: Int = 0) { def getAndIncrement(): Int = { value += 1 value } def get(): Int = value } // 一つのSequence を複数のスレッドから操作すると? class SequenceTask(sequence: Sequence) { // ... override def run() = { val threadId = Thread.currentThread().getId() while(!isTerminated) { val count = sequence.getAndIncrement() println(f"$threadId%04d:$count%05d") } } } 27

Slide 28

Slide 28 text

28

Slide 29

Slide 29 text

+= は 以下の3 つの操作となる。 値を読む それに1 を加える 値を更新する リード・モディファイ・ライト操作 これらの操作は、それぞれが独立していて、直前の状態に依 存している += , -= 以外でもありうる チェック・ゼン・アクト操作も競り合い状態になりやすい 状態をチェックし、その結果に基づいて、何らかのアクショ ンを行う操作 29

Slide 30

Slide 30 text

class Sequence(private var value: Int = 0) { def getAndIncrement(): Int = synchronized { value += 1 value } def get(): Int = synchronized { value } // getAndIncrement 後のvalue を可視とするためsynchronized を適用 // そうしない場合は不要 } 30

Slide 31

Slide 31 text

命令の順序替えについて 最適化により、a = 1 よりx = b が先に実行される可能性がある ( 同じ結果を導出できるセマンティクスであれば、計算プロセス を最適化してもよいという前提) a = 1 x = b この最適化を知らないと並行プログラミングでは問題になること がある。 31

Slide 32

Slide 32 text

var x: Int = 0; var y: Int = 0; var a: Int = 0; var b: Int = 0 def threadTest(testNo: Int) = { val startLatch = new CountDownLatch(1) x = 0; y = 0; a = 0; b = 0 val ta = new Thread({ () => startLatch.await() a = 1 x = b }) val tb = new Thread({ () => startLatch.await() b = 1 y = a }) ta.start(); tb.start() startLatch.countDown() ta.join(); tb.join() if (x == 0 && y == 0) { // 思い込みに反して (x, y) = (0, 0) が存在する println(s"t = $testNo, x = $x, y = $y") } } 32

Slide 33

Slide 33 text

順序替えの対策 スレッド間で共有する変数はvolatile にする( そもそもスレッド 間で変数を共有しない方がよりよい) が、アトミック性がないので、(1, 0), (0, 1), (1, 1) が発生する 33

Slide 34

Slide 34 text

Scala scala.concurrent.Future 非同期に行われる計算結果を返すオブジェクト(Immutable) scala.concurrent.Future は、ポーリングやブロッキングを行 わずに関数の結果を組み合わせることが可能。 java.util.concurrent.Future とは違うAPI(Future#get は、ポー リングとスレッドのブロックが生じる) akka-actor 状態を保持して、その状態に基づいて異なる振る舞いをする 場合はアクターの方が使いやすい エラーカーネルパターンを使って障害に備えることができる 34

Slide 35

Slide 35 text

まとめ やはり、スレッドを直接操作することは難しい。 scala.concurrent.Future, akka-actor などの高レベルなAPI を使 える方がよいが、使い方を謝れば同様の問題が生じるので、基 本的な概念は知っておいて損はない 35

Slide 36

Slide 36 text

参考文献 『Java 並行処理プログラミング』 『Effective Java 第2 版』 『Java 言語仕様 第3 版』 36