Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Javaスレッドプログラミングの基礎

 Javaスレッドプログラミングの基礎

スレッドプログラミングのハマりどころを中心にどうするとよいのかをまとめたスライド。

933291444e456bfb511a66a2fa9c6929?s=128

かとじゅん

May 11, 2018
Tweet

More Decks by かとじゅん

Other Decks in Programming

Transcript

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

  2. ムーアの法則とは 2

  3. 「半導体の集積密度は18 ~24 カ月で倍増し、チップは処理能力 が倍になってもさらに小型化が進むという法則( 経験則) 」。 1965 年に、インテル社創設者の1 人であるゴードンムーアが提 唱

    つまり、1 年半から2 年でIC チップに集積されるトランジスタ 数が倍増していく( プロセスの微細化とも呼ばれる) ということ 微細化が滞るとコンピューティングの進化も止まってしま う、と考えられていた 3
  4. ムーアの法則を維持する戦略 クロック周波数をあげる トランジスタの集積度をあげ、1 秒間で実行できる命令数を増 やす戦略。クロックとはCPU の動作基準となる時間の単位。 3GHz は1 秒間で30 億回のクロックを表す

    ハードウェアを改良すればソフトウェアのパフォーマンスも 改善された時代 CPU に搭載するコアを複数にする クロック数アップ戦略は発熱やエネルギー効率の問題が障害 となり、現在では複数のタスクをそれぞれのコアが並列 (Parallel) に処理できる、マルチコアが一般的になっている 4
  5. FYI: 並行処理と並列処理 並行(Concurrent) は、「複数の動作が、論理的に、順不同もし くは同時に起こりうる」こと 1CPU で複数の仕事を並行処理するには、処理時間を非常に短 い時間単位で分割する、タイムスライス( タイムクォンタム) が

    利用される。ミクロな観点では同時ではない 並列(Parallel) は、「複数の動作が、物理的に同時に起こるこ と」 - 目的は計算速度の向上。スレッドをコアに割り当てて、並列 にタスクを処理させる必要がある ※本スライドでは、この二つの概念を特に区別しない場合は、 並 行 を一般的な概念を示す用語として利用する 5
  6. マルチスレッドは難しい? スレッドAPI を使えば並行処理は実現可能だが、陥りやすい代 表的な問題について考えてみよう タスク量に比例してスレッド数が増加する問題 処理中のタスクを安全に中断できない問題 競り合い状態や命令の順序替えによって想定外の挙動をする 問題 6

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

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

  9. タスクの要求毎にスレッドを生成する例 class CommandExecutor { def execute(cmd: () => Unit): Unit

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

    private val pool: ExecutorService = Executors .newFixedThreadPool(nThreads) def execute(cmd: () => Unit): Future[_] = { // タスクをスレッドプールにサブミットする pool.submit(() => cmd()) } } 10
  11. スレッドプールのイメージ Google 画像検索 11

  12. Executors のファクトリ newFixedThreadPool 固定サイズのスレッドプールを作る newCachedThreadPool サイズ制限なしのスレッドプール。スレッド数は要求によっ て増減 newSingleThreadExecutor 1 つのワーカースレッドのみ。逐次処理向き

    newScheduledThreadPool タスクの遅延開始と周期的実行をサポート 12
  13. 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
  14. 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
  15. タスクを安全に中断できない問題 15

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

  17. メモリモデル: 可視性の問題 スレッド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
  18. 以下のコードに変換されてしまう( 巻き上げ最適化) if (!stopRequested) while(true) { i += 1 }

    Java メモリモデルでは、スレッドごとに最適化するため、値の 変更が他のスレッドへ即座に伝わらない可能性がある デフォルトではスレッドからの書き込みが、別のスレッドが読 み込める保証( 可視性) がない。@volatile を使えば、異なるスレ ッドからも読めるようになる @volatile private var stopRequested = false // 巻き上げ最適化はされない 18
  19. 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
  20. 固有ロック(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
  21. 固有ロック(synchronized) 固有ロック(synchronized) には、アトミック性を保証する以外 に、可視性を保証する機能もある 事前発生(happens-before) の原理で可視性を保証 あるスレッドが取得したロック内で行ったすべての操作は、 異なるスレッドがロックを取得するとき、可視となる。 参照: 「Java

    言語仕様3 版」の「17.4.5 先行発生の順序」 21
  22. ブロッキング中にスレッドを停止する例 フィボナッチ数列をブロッキングキューに追加するタスクを停止 させる 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
  23. // フィボナッチ数列をブロッキングキューに追加するタスク 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
  24. ブロック中はフラグを変えても終了できない。この例ではタスク を実行するスレッドに対して、インタラプションを発生させてタ スクを中断させている。 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
  25. 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
  26. 想定外の挙動をする問題 26

  27. 競り合い状態 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
  28. 28

  29. += は 以下の3 つの操作となる。 値を読む それに1 を加える 値を更新する リード・モディファイ・ライト操作 これらの操作は、それぞれが独立していて、直前の状態に依

    存している += , -= 以外でもありうる チェック・ゼン・アクト操作も競り合い状態になりやすい 状態をチェックし、その結果に基づいて、何らかのアクショ ンを行う操作 29
  30. class Sequence(private var value: Int = 0) { def getAndIncrement():

    Int = synchronized { value += 1 value } def get(): Int = synchronized { value } // getAndIncrement 後のvalue を可視とするためsynchronized を適用 // そうしない場合は不要 } 30
  31. 命令の順序替えについて 最適化により、a = 1 よりx = b が先に実行される可能性がある ( 同じ結果を導出できるセマンティクスであれば、計算プロセス

    を最適化してもよいという前提) a = 1 x = b この最適化を知らないと並行プログラミングでは問題になること がある。 31
  32. 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
  33. 順序替えの対策 スレッド間で共有する変数はvolatile にする( そもそもスレッド 間で変数を共有しない方がよりよい) が、アトミック性がないので、(1, 0), (0, 1), (1,

    1) が発生する 33
  34. Scala scala.concurrent.Future 非同期に行われる計算結果を返すオブジェクト(Immutable) scala.concurrent.Future は、ポーリングやブロッキングを行 わずに関数の結果を組み合わせることが可能。 java.util.concurrent.Future とは違うAPI(Future#get は、ポー リングとスレッドのブロックが生じる)

    akka-actor 状態を保持して、その状態に基づいて異なる振る舞いをする 場合はアクターの方が使いやすい エラーカーネルパターンを使って障害に備えることができる 34
  35. まとめ やはり、スレッドを直接操作することは難しい。 scala.concurrent.Future, akka-actor などの高レベルなAPI を使 える方がよいが、使い方を謝れば同様の問題が生じるので、基 本的な概念は知っておいて損はない 35

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

    版』 36