セカイノカタチ

世界のカタチを探求するブログ。関数型言語に興味があり、HaskellやScalaを勉強中。最近はカメラの話題も多め

イマドキの仮想通貨採掘用ASICでパスワード総当たりをすると突破まで何日かかるのか?

タイトルの通りなのですが、最近流行の仮想通貨を掘るための専用設計されたASICと呼ばれる機器があります。

こんな感じのものですが。

これは、1秒間に20テラ回のハッシュ計算ができるとされており、20テラとは20兆のことなので、このマシンがあれば、1秒間に20兆回のハッシュ計算ができます(語彙力)。

通常、Webサイトやコンピュータに保存されているパスワードというのは、ハッシュ計算という方法を使って暗号化*1されています。そのため、ASICを使えばパスワード総当たりができます*2

これは、パソコンのCPUやGPUを利用したハッシュ計算と比べてけた違いに強い計算能力なので、今まで安全圏とされていたパスワード長でも脅威となりうる可能性があると思い、ちょっと調べてみました。

実際3年前のブログでは、3000万件/秒を基準に計算しているので、実にその60万倍以上の計算能力となります。恐ろしいですね(というか数年おきにパスワード強度の計算をしているのか・・・)。

qtamaki.hatenablog.com

注意点としては、単純計算なので、実際にアタックした場合、誤差がある可能性があるのと、ビットコイン用のASICをそのまま、パスワードクラックに使えるわけではないということです*3。あと、コンピュータのパワーをフルに使って総当たり攻撃ができるシチュエーションは限られています。Web上のパスワードを外部から攻撃する場合、せいぜい数十回から数百回程度しかトライできないはずなので「password」とか「12345」とかの単純なパスワードを避けていれば、攻撃される可能性は低いです。

ということで、行ってみましょう。

計算してみる

まずは、文字種別ごとに何種類の文字があるのか調べます。

文字種別(省略形) 種類 内容
数字(0) 10 0123456789
小文字(a) 26 abcdefghijklmnopqrstuvwxyz
大文字(A) 26 ABCDEFGHIJKLMNOPQRSTUVWXYZ
記号(!) 33 !"#$%&'()*+,-./:;<=>?@[]^_`{|}~

こうしてみると、英語の大小文字より、記号のほうが種類が多いです。パスワードを設定するときは、記号を織り交ぜると強度が飛躍的に増します。おすすめです。

そして、単純に同じ文字種別を使ってパスワードを作成した場合の強度です。何通りあるかという話です。

文字種別 種類 8文字
a 26 208,827,064,576 200億
0 10 100,000,000 1億
! 33 1,406,408,618,241 1兆

単純に計算すると、文字種別が多い方がパターンが多くなります。数字のみのパスワードは非常に脆弱なので危険ですね。

それでは、各文字種および、組み合わせによるパターン数を数えます。

文字種別 種類 8文字 9文字 10文字 11文字 12文字
a 26 208,827,064,576 5,429,503,678,976 141,167,095,653,376 3,670,344,486,987,780 95,428,956,661,682,200
0 10 100,000,000 1,000,000,000 10,000,000,000 100,000,000,000 1,000,000,000,000
! 33 1,406,408,618,241 46,411,484,401,953 1,531,578,985,264,450 50,542,106,513,726,800 1,667,889,514,952,990,000
aA 52 53,459,728,531,456 2,779,905,883,635,710 144,555,105,949,057,000 7,516,865,509,350,970,000 390,877,006,486,250,000,000
a0 36 2,821,109,907,456 101,559,956,668,416 3,656,158,440,062,980 131,621,703,842,267,000 4,738,381,338,321,620,000
a! 59 146,830,437,604,321 8,662,995,818,654,940 511,116,753,300,641,000 30,155,888,444,737,800,000 1,779,197,418,239,530,000,000
aA0 62 218,340,105,584,896 13,537,086,546,263,600 839,299,365,868,340,000 52,036,560,683,837,100,000 3,226,266,762,397,900,000,000
aA! 88 3,596,345,248,055,300 316,478,381,828,866,000 27,850,097,600,940,200,000 2,450,808,588,882,740,000,000 215,671,155,821,681,000,000,000
aA0! 98 8,507,630,225,817,860 833,747,762,130,150,000 81,707,280,688,754,700,000 8,007,313,507,497,960,000,000 784,716,723,734,800,000,000,000

横幅が足りずに完全にはみ出していますが・・・。^^;

とりあえず、一見膨大なパターンの組み合わせがあり、これを手当たり次第にトライするのは無謀なように見えます。しかし、我らがASIC君は、1秒間に20兆回の計算ができます。この計算能力を使ってクラックした場合、どの程度の時間でパスワードを突破できるのでしょうか?(単位は「日」です)

文字種別 種類 8文字 9文字 10文字 11文字 12文字
a 26 0 0 0 0 0
0 10 0 0 0 0 0
! 33 0 0 0 0 0
aA 52 0 0 0 2 113
a0 36 0 0 0 0 1
a! 59 0 0 0 9 515
aA0 62 0 0 0 15 934
aA! 88 0 0 8 709 62,405
aA0! 98 0 0 24 2,317 227,059

というわけで、殆どのケースであっさり「0日」で突破できます。流石のパワーですね!

これを見ると、小文字と記号を組み合わせて12文字か、大小文字と記号を組み合わせて11文字ぐらいが安全ラインでしょうか。1年以上かかりそうです。

このASICを1年間休まず稼働させると、電気代が300~500万円ぐらいかかりそうなので、あなたのパスワードがそれに見合うだけの価値があるなら頑張ってクラックされる可能性がゼロとは言えない?

まとめると

パスワードの強度的には、英数記号を織り交ぜた方が、パターン数が増えるので強度が増します。特に記号が有効です。

総当たりされるシチュエーションは限られていますが、それでも安全なパスワードとなると、小文字+記号で12文字以上か、大小文字+記号で11文字以上が最低ラインとなります。できれば13文字以上のパスワードを使うと良いと思います。

*1:正確には暗号化とは違いますが簡単のため

*2:正確にはSHA-1というハッシュアルゴリズムの計算しかできないので、パスワードのハッシュアルゴリズムがSHA-1でないと使えません

*3:実際にやろうとして「できないじゃないか!」と言われても責任は負いかねます。^^;

Oracle JDK 11のリリースと有償化について

さて、以前から有償サポートのみとなったことが話題になっている、Oracle JDK ですが、初のLTS(長期間サポート)となる、JDK 11 がリリースされました。

JDK 11 Release Notes, Important Changes, and Information

有償サポートになったからといって、お金を払わないと入手できなくなったわけではなくて、セキュリティパッチなどの公式アップデートが入手できなくなったということみたいです。

下記ダウンロードページに行けば、普通にダウンロードできます。

Java SE Development Kit 11- - Downloads

ライセンス上も、今までと同様に開発に使用することができるようです。ライセンスページが英語なため翻訳して読んだので、正確なところが理解できているか不安ですが、概略としては「アプリケーションの開発、テスト、プロトタイプ作成およびデモンストレーション」ができるとあります。

Oracle Java SE License

今までのOracle JDKだと、リリース後数回の公式アップデートが提供されてきました。例えば、JDK 8 だと、JDK8u181 のように、バージョン末尾に「u181」のような数字が付いた形で配布されます(181回リリースされたわけではないです。番号は飛び飛びです)。

JDK 11以降、無償のアップデート提供は完全に停止し、JDK8のようなアップデートの提供を受けるためには、 「Java SE Subscription」という月額利用料を支払うサポートサービスに加入する必要があるようです。費用は、「1ユーザー300円以下」とあります。買いたい人は、営業担当に連絡するという、古式ゆかしい、完全にエンタープライズな販売チャネルしか持っていないようで、AWSやGoogle AppsやOffice365のような、個人&SOHOを含めた一派ユーザー向けのサービスではないようです。

オラクル Java SE Subscription FAQ

この「Java SE Subscription」に加入していない場合、アップデートを受けられないので詰んだという声が、主に企業(や官公庁)の調達部門方面から聞こえてきそうですが、実際には、これがそれほど重要なわけではありません。

以前にブログでも解説しましたが、OpenJDKという、中身が同じ(JDK11からまったく同じになったらしい)双子の兄弟がいまして、そちらは無償で利用できます。

アップデートの提供は、半年間*1だけですが、JDKのアップデートに追従すれば、最新のセキュリティとコンパイラ機能が提供されるので、そちらを利用すれば実質的には困らないと思います。

「そんなこと言ったって、セキュリティがー」という心配をされるかたもいるかとは思いますが、JDKとJREのアップデートは別物だということを考慮する必要があります。

今回サポートが有償化になったのは、基本的にはJDKという開発者が使う、コンパイラや解析ツール類を指していますが、完成したアプリケーションを実際に利用者が使用するのは、JRE(Java Runtime Edition)という実行環境だけがあればよいのです。

JREは、「同じバージョンか、古いバージョンのコンパイラでコンパイルされたバイナリ(classファイル)なら動く」という互換性がありますので、JDKのバージョンを上げることができなくても、JREのバージョンさえ上げておけば、実行時のセキュリティは担保されるということになります。

つまり、乱暴な言い方ですが、「開発環境は古いままで実行環境だけ最新版を入れろ」ということです。特にユーザー部門や官公庁なんて、自分たちで開発するわけじゃないから、JDKのサポートなんて気にしても仕方ないのではないかと思います。

ちなみに、ソースコードとJDKとJREのバージョン間の互換性ですが、概ね下図のようになっています。

f:id:qtamaki:20180926182107p:plain

青い線は問題なく実行できるラインで、同一バージョン間の運用(ソース→コンパイラ→JREが同じバージョン)が問題ないのは当然として、ランタイム時には、古いバージョンのJDKでコンパイルされたバイナリの実行がサポートされます。

一番右下のJRE11の部分に「?」マークがついていますが、これは、現時点でJRE11がリリースされていないので、よくわかってません。^^;

まさか、JREをリリースしないなんてことはないと思いますが、軽くググった感じでは情報が無かったので、現状では何とも言えません。

Java9から入ったプロジェクトジグソーにより、必要最低限な実行環境をパッケージに含めることができるようになったため、「ランタイムを別途配布するのではなくパッケージに含めろ」というのがJava開発サイドからのメッセージらしいですが、JREを配布しないというのは流石に・・・まさか・・・ね?

まあ、最悪の場合、最新のJDKにオプション(-source, -target)をつけることで古いバージョンを模倣することができますので、そちらを利用すれば古いソースコードでもコンパイルできるし実行できるので、何とかなるんじゃないかと思います(急にトーンが下がる)。

まとめると

  • Oracle JDK11がリリースされた
  • 無償のアップデートは提供されない
  • Open JDKを使うか、ランタイムだけ最新版を入れるという回避策がある
  • 最悪コンパイラのオプションで古いバージョンをエミュレートすれば・・・

ということで、ビバ!ジャヴァ!

*1:オラクルの人は「少なくとも」半年間という表現をしているようです: http://mail.openjdk.java.net/pipermail/jdk-dev/2018-August/001824.html

プログラミング教育より大切なことなんて無い

こんにちは。

タイトルは、流石に大袈裟ですね。

こちらの記事を読んでの感想文と、プログラミング教育に対する僕の気持ちです。

www.moneypost.jp

子供のときから単にプログラミングが書けること自体にはあまり価値はありません。IT関係の仕事で価値があるのはシステムを作れることです。プログラミングは、自分が論理的に考えたシステムを表現するための手段にすぎません

プログラミングとは、何なのでしょうか?

車や洗濯機のように、「何かをするための道具」という見方をすることも、もちろんできます。

しかし、プログラミングは、それ自体に価値のある行いだと僕は思います。

「絵が描けること自体にあまり価値はありません。絵は自分自身が考えたイメージを表現するための手段に過ぎません」と言い換えた時に、絵を描くことと表現することの間に、優劣や大切さの違いは感じられません。

「プログラミングとは何か?」という問いには、様々な答えがあります。

ひとつには、論理的な命令とフィードバックのループです。電卓を叩くと即座に答えが出ますが、このフィードバックの容赦ない即答性が機械やコンピュータの良い所です。

プログラミングは、電卓や車の運転と比べて、はるかに複雑な論理的な命令の組み合わせでできており、その組み合わせを冷徹なまでに正確にコンピュータは実行しようとします。人間の持つアバウトさとコンピュータの正確さの間には大きなギャップが存在するため、不慣れた操縦者(プログラマー)は、途端に大量のエラーを巻き起こします。

プログラミング以外の方法で、この常識を覆すほどの「トライ&エラー」のスピードを体験する方法を僕は知りません。

このループは、明らかに人類の叡智を次世代へと推し進めます。コンピュータを通してありありと体験した逆説的で論理的な世界は、人類の覚醒を促し、世界の構造そのものをひっくり返す力があります。

それは、単に「便利だから」というだけではなく、「フィードバック」そのものの持つ力ゆえです*1

「プログラミングより、アイデアや何をしたいかという目的が大事」というのは、一見耳障りが良く、正論に聞こえますが、間違っています。

現代、そして未来において、良いアイデアを発揮する為には、プログラミング教育の経験が必須なのです。

プログラミングは、アイデアを実現する手段かもしれません、しかし、そのアイデアはプログラミングすることによって、より先進的で逆説的なものへと進化していきます。

より先進的なアイデアは、よりよいプログラミングへのチャレンジを促し、それがまたアイデアを生むというループが存在するのです。

落合氏は、現代の魔術師と呼ばれる人ですので、プログラミングがアートであることや、アイデアとプログラミング(及びプロダクトの実現)が、相乗効果を生む関係にあることは、理解されていると思います。

それでも、このような苦言を述べているのは、プログラミング自体の表面だけを捉えて、子どもたちに、単に「良い就職のために」というような矮小な目的を押し付けてプログラミングさせることへの危惧があるのではないかと思います。

その点に関して、僕は全く心配していません。

何故ならば、プログラミングは「楽しい」からです。

子どもたちに必要なのは、キッカケです。

キッカケさえあれば、好きな子は勝手にドンドン自分で進んでいきます。

今、世の中を支えているプログラマーは、プログラミングが好きでやっている人がほとんどだと思います(嫌いなやつは全員逃げたか死んだよ。辛すぎてな。という生存バイアスが掛かっているかも)。

面接などをしていて非常に残念に思うのは、「大学の時にプログラミングに触れて面白いと思いました」とか「前職でマクロを組んでみて、非常に面白いと感じ転職を考えています」という人達が、とても多いことです。

プログラミングに触れて興味を持つ事自体、とても素晴らしいことですが、もっと早く触れていたらもっと良かったのに!と、勿体無い気持ちでいっぱいになります。

僕自信も、プログラミングに触れたのは高校生になってからです(1990年台ではそれでも珍しかった)。

彼らや僕が、小学生の頃にプログラミングに触れていたら、世界はもっと変わっていたように思います。

今からでも遅くはないのです。

すぐにプログラミング教育を始めて、より多くの人が、より若い年代でプログラミングに触れられるようになることを切に願います。

プログラミング教育より大切なことなんて無いのです。

Kids heart OLPC (no matter what the critics say)

*1:フィードバックにより、物事が進展した例はごまんとあります。スポーツトレーニングでは、何が良くて何が悪いのか、すばやくフィードバックすることが重要な要素になっていますし、将棋なんかでも、コンピュータを使って手の強さを解析することが、研究の主体となっています。ビジネスにおいても、「フィードバックをいかに受けるか」という課題が重要な関心事で、それ自体で本が一冊書けるほど、深淵はテーマとなっています

Android で Volley の RequestFuture を使うと TimeoutException

Android開発をしていて、バックエンドのサーバーにHTTPリクエストを投げるというのは、ありふれたシチュエーションだと思いますが、 Volley というGoogleおすすめのライブラリを使っていてちょっとハマりました。

Volley でHTTPのリクエストを投げるのはこんな感じの処理になります。

val url = "http://hogehoge"
val queue = Volley.newRequestQueue(context.getApplicationContext())
queue.add(JsonObjectRequest(Request.Method.POST, url, JSONObject("{userId: $email, password: $password}"), { 正常処理 }, {エラー処理}))

で、 {正常処理} と {エラー処理} は、それぞれコールバックが入るのですが、面倒なので良い感じに処理してくれて結果を返してくれる RequestFuture といクラスがあります。

val url = "http://hogehoge"
val queue = Volley.newRequestQueue(context.getApplicationContext())
val rf: RequestFuture<JSONObject> = RequestFuture.newFuture()
queue.add(JsonObjectRequest(Request.Method.POST, url, JSONObject("{userId: $email, password: $password}"), rf, rf))

rf.get(10000, TimeUnit.MILLISECONDS) // ここでデッドロックする

このコードは、さくっとデッドロックして java.util.concurrent.TimeoutException を返します。

ちょっと調べると下記のStackOverFlowが引っかかるのですが、誰も回答をくれないので質問者本人がアレコレ試して別スレッドで結果を受け取ったら動いた。という感じの流れになっています。

stackoverflow.com

ちょっと釈然としないので、コードを追ってみました

RequestQueueはUIスレッドで実行される

Volley の RequestQueue::add は、標準でUIスレッドにHandler経由でpostするだけの実装でした。

public RequestQueue(Cache cache, Network network, int threadPoolSize) {
    this(cache, network, threadPoolSize,
            new ExecutorDelivery(new Handler(Looper.getMainLooper())));
}

new Handler(Looper.getMainLooper())) で、Handlerを生成して ExecutorDelivery の中でpostしています。

RequestFuture は Threadの事は考えず get() 内部で wait() している

RequestFuture は、ただのクラスなんですが、 get()を呼び出すと、なんやかんややって wait() します。そして、 RequestQueue から onResponse() が呼ばれるのを待ちます。

そして、 RequestQueue は、UIスレッドでレスポンスを処理して onResponse() を呼び出すので・・・。

UIスレッドで RequestFuture のget()を呼び出すと、UIスレッドでレスポンスを処理しようとする RequestQueue とだだかぶりじゃん!?!?!?

結論

RequestFuture の get() は、UIスレッド(mainスレッド)で呼んではいけない!

という結論でした。AndroidのThread周りは複雑だな・・・。

※ 同じハマり方をしている人が見つけやすいようにタイトルにキーワードをふんだんに盛り込んであります

Toneo Voley Playa Villananitos 2017

Javaランタイム(JRE)のサポートと互換性について

qtamaki.hatenablog.com

先日、Oracle Javaの今後のサポートについて考察したのですが、1点漏れていた議論がありました。

Javaのランタイムエディション(JRE)のサポートについてです。

結論から言うと、JREは強い後方互換を保ってアップデートされるため、「常に最新版を使い続けろ」です。最新版を使っていればパッチが提供され続けます。

この点について、ちょっと複雑で誤解している人もいるかもしれないので、簡単に説明します。

  1. 過去のJavaソースコード → 最新のJDKではコンパイルできない(だいたい)
  2. 過去のJavaでコンパイルしたClassファイル → 最新のJREで実行可能
  3. 最新のJDKでコンパイルしたClassファイル → 過去のJREでは実行できない
  4. 過去の外部ライブラリでコンパイルしたClassファイル → 最新の外部ライブラリでは動かないかもしれない

よく、「Javaはバージョン間の互換性がない」という言葉を聞くことがありますが、実際には「1.」を指す場合が多いと思います。

確かに、Javaは、ソースコードレベルでは、新しい構文が追加され、過去の書き方が一部変更になったりするので、コンパイルが通らなくなります。しかし、それはコンパイラのバージョンによる非互換なため、ランタイムの非互換とは別の話になります。過去にコンパイルしたClassファイルを使うのであれば、最新JREで問題なく*1動くはずです。

逆に、最新のJDKでコンパイルしたClassファイルを過去のJREで動かそうとするとバージョンチェックに引っかかり動きません。最新のClassファイルには、最新のJREの機能が必要かもしれないからです。これは、素直に実行環境のJREを最新版にしましょう。それで問題ないはずです。

蛇足ですが、apache Commonsなどの外部ライブラリを利用している場合、こちらのバージョンが変更するとエラーになる可能性がありますが、これはJRE/JDKとは別の話になります。mavenなどを利用して、jarのバージョンを統一すれば回避できます。

今回のOracle JDK有償化との関係は?

ということで、「Oracle JDKが有償化した場合にJREはどうなるのか?」ですが、オラクル社はあまり気にせずに最新版のJREを提供し続けるのだと思います。

こちらは半年サイクルとは別に、セキュリティアップデートがあれば、必要なタイミングでアップデートされるでしょう。

そもそも、JDK側は開発者が開発環境で使用するため、セキュリティアップデートについてもランタイム側ほど優先度が高くありません(低いってわけでもないけど、比較的ってことです)。

そのため、半年ごとのメジャーアップデート以外で、セキュリティパッチが出ることはあまり考えられないと思います。

「古いバージョンを使っていても、重大なセキュリティホールがあればアップデートされるんでしょ?」という意見は見当違いという事になります。

結論

古いJDKを使っていても、JREは最新版を使おう

もうそれでいいでしょ・・・*2

*1:厳密には挙動が変わるかもしれませんが

*2:ランタイム時にjavacを呼び出すPlayframeworkとかいうフレームワークのことは考えないものとする

Oracle Javaの無償配布終了で何が起こるのか?

www.publickey1.jp

世界中で30億デバイスに搭載されているプログラミング言語Javaのリリースサイクルが変更になることと、オラクル版のJavaが無償提供されなくなるというニュースです。

リリースサイクルが半年ごとに

オープンソースのOS、LinuxのメジャーなディストーションであるUbuntuなどで採用されているリリース方式で、「機能ができたらリリースする」のではなく、「時間が来たらリリースする」方式です。

Javaぐらい大規模で歴史も深いプロジェクトになると、影響範囲の検証などをじっくり行うため、大きな機能改善などは、何年もかかったりします。すると、自然とリリースサイクルは長くなり、Java7からJava8までで、2年半、Java8→Java9では、3年半もかかってしまいました。

機能ベース = いつリリースされるかわからない

このことを回避するために、「リリースペースを固定する」という方法を採ることにしたようです。

半年に1回のリリースであれば、小さな機能改善をコンスタントに提供できるため、ユーザーの利便性は増します。

大規模な機能変更は、数年に一度になると思いますが、それは今までと同じなのでデメリットにはならないわけです。

そして、Ubuntuなんかもそうですが、リリースサイクルが決まっていると「この機能を次のリリースに入れたい!」というモチベーションが湧くので、機能の開発者にとっても目標ができて良いのかもしれません。

時間ベース = コンスタントな機能提供、開発モチベーション

Oracle JDKとOpenJDKが同じ機能に

そもそも、Oracleの提供しているJDKとOpenJDKは、機能的な差異はないみたいです。

歴史的には、別々のものでコンパイラの性能にも差異があったと記憶していますが、現在はコアとなる機能は全く同じで、プロファイラなどの周辺ツールが付属するかしないかの違いしか無いようです。

そして、この周辺ツールについてもオープンソース化を行い、OpenJDK に付属させるということみたいです。

これは、グッドニュースですね。

Oracle JDK = OpenJDK になる

Oracle JDK は有償に

そして、物議を醸しているのが、Oracle JDK の有償提供というニュースです。

文章を読むと、 Oracle JDK と OpenJDK を統一して、リリースラインを1本にするということと、リリースサイクルを半年ごとにするということに合わせて、Oracle版のJDKは3年毎に8年以上のサポート期間を持つ、長期サポートバージョンをリリースして、有償サポートする。ということみたいです(通常版は半年サポートというか、次のバージョンまで)。

Oracle JDK と OpenJDKの統合
+
リリーサイクルの固定
+
長期サポート版の有償提供

これらのことは、バラバラに起こるのではなく、全てが連動した施策という事になり、全体を見ると歓迎すべき方向性なのではないかと思います。

何が起こるのか?

これによって、「何が起こるのか?」ですが、大企業の社内向けシステムなどは、一度構築したシステムを長期に利用し、その間にセキュリティパッチが提供されることなどが、システム選定時の稟議に関わりますので、恐らく長期サポート付きの有償契約を選択するのではないかと思います。3年毎の長期サポート版を買えば、8年以上の長期に渡ってサポートを受けられるというのは、願ってもないことなはずです。

→大企業は有償サポートが嬉しい(はず)

そして、常に開発を続けているウェブサービスや、ベンチャー企業などは、リリースサイクルの合わせて自社のシステムをアップデートしていくのだと思います。現代のシステム開発では、「停滞=死」ですから、定期的に新機能がリリースされる方が嬉しく、今まで有償だったツールが無償になるのもお得感しか無いはずです。

→ベンチャーは定期アップデートが嬉しい(はず)

そして、システム開発と無縁な中小企業ですが、今までもJavaのアップデートなんて気にしてなかったでしょ?それこそ、Java1.4とかJava5とかを平気で使っているはずなので、これからも気にせず、システム導入時にリリースされている最新版を使って、それ以降は放置になると思います。

→中小企業は気にしない(はず)

感想

全体的には、それぞれのセクションで満足度の高い、良い戦略なんじゃないかと思います。

さすがはオラクル、頭が良い人が考えた感じがします。

むしろ、ひょっとして、UbuntuもLTSは有償提供が良いのでは・・・?

The Evolution of Computer Programming Languages #C #Fortran #Java #Ruby

追記

ブクマで指摘がありましたので、 Open JDK → OpenJDK にしました。

ScalaのFutureに渡す ExecutionContext の挙動がわかりにくいので図解する

Scalaには、非同期実行をサポートするために Future というクラスがあります。

正確にはJavaにも同名の仕組みがあるのですが、それをちょっと便利にした感じのものです。

Futureの仕組みを簡単に言うと、関数を渡すと別のThreadでその関数を実行してくれるものです。

それだけだと、Threadクラスとあまり変わらないのですが、ExecutionContext という管理クラスを渡してあげることによって、Threadの挙動をよりインテリジェントに制御してくれるため、Thread周りの煩わしさが大きく軽減します。

そのThread制御を担当するのが、ExecutionContextExecutorServiceというクラスで、こいつは ThreadPoolExecutor など、ExecutorService インターフェイスの実装オブジェクトをプールとして利用して生成されます。今回図解するのは、ExecutionContext のデフォルト実装になっている ThreadPoolExecutor を対象としますが、ExecutionContext の別の実装である ForkJionPool に関しても追記にて簡単に説明します。

ただし、ForkJoinPool はパフォーマンス特性に癖があるようですので、Scala(およびJava)のFutureの挙動を制御するためには、まずは、ThreadPoolExecutor を第一候補に考えておけば良いと思います。

ThreadPoolExecutor の挙動

だいぶ端折ったのですが、それでも前置きが長くなりました。

d.hatena.ne.jp

ThreadPoolExecutor の挙動については、こんな記事を見つけて、これが詳しいので参照すれば良いだけなのですが、一点だけ腑に落ちない部分があります。

それは、ThreadPoolExecutor のコンストラクタに渡す、maximumPoolSize というパラメータの挙動です(と連動してkeepAliveTimeの挙動も)。

この挙動を知るためには、ThreadPoolExecutorのjavadocを紐解く必要があります。

ThreadPoolExecutor (Java Platform SE 8)

この説明の「キューイング」の項目に詳しく説明があるのですが、ゴチャゴチャ書いてあってわかりにくいので図解します。

まず、ThreadPoolExecutor は重要なパラメータとして、1.Theadプールの初期値(corePoolSize), 2.最大プールサイズ(maximumPoolSize), 3.Queueの容量。の3つがあります。

ThreadプールのサイズとQueueの容量は、このように動作します。ここまでは直感的でわかりやすい挙動だと思います。

面白いのはこの後で、Queueの容量を越えたタスクが投入されたときです。

このように、後から投入されたタスクのためにThreadプールが拡張され、そのTheadを使って実行されます。そのため、実行順としては先に投入したタスクよりも先に実行されるという挙動をします。

そして、この時拡張されるTheadプールの最大値を決めるのが、maximumPoolSize となります。なお、拡張されたThreadプールは、実行するタスクが無くなって keepAliveTime が経過すると破棄されます。

次に、Theadプールが maximumPoolSize に達した状態で更にタスクが投入された場合の挙動です。

システムエラーになります。

正確に言うと、この挙動は RejectedExecutionHandler を渡すことによって制御できるのですが、Futureの場合投げっぱなし(ノンブロッキング)が基本となりますので、この挙動をカスタマイズするのは注意が必要です(DiscardPolicyにしたらタイムアウトまで返ってこなくなった、CallerRunsPolicyは実行が遅くても処理継続されるがFutureがシステム資源を食い尽くす可能性がある)。

ということで、現実的な路線で取り得る選択肢は3つに絞られると思います。

この内パターン3は例外発生の危険性があります。とはいえ、Queueを無制限にしてもThreadを無制限にしても、メモリーやらThreadやらのリソースを消費するわけで、挙動の読めないエラーが発生するぐらいなら、いっそ「意図的にウイークポイントを作っておく」という戦略もありかもしれません。

結論: Executors が提供する ThreadPoolExecutor を使おう

ごちゃごちゃわかりにくいので、java.util.concurrent.Executors が提供するできあいの ThreadPoolExecutor を使っていれば良いという結論に達しました。

使うべきは2つです。

newFixedThreadPool(int nThreads)

「Threadプールが固定長=Queueが無制限」なプールを生成します。Threadプールのサイズを決められるので一番使い勝手が良いと思われます。Queueに積まれるタスクは、所詮はタダのオブジェクトなので、メモリー以外のリソースを消費しません。Thread数をシステム資源を圧迫しない程度の常識的な値に設定してメモリーをたんと積めば良いだけなので、リソース管理が楽ですね。

newCachedThreadPool()

Threadプールを無制限に拡張するプールを生成します。Queueサイズはなんと「ゼロ」です。そのため、投入されたタスクは即座にThreadプールに投入され、プールが足りなければ無限に拡張されます。スレッドプールの初期サイズも割り切りの「ゼロ」です。Queueに投入されてしまったタスクは「何もしない」ので実行効率が悪く、Threadに投入されていれば、OSレベルでタスク制御されるので、効率が上がります。ただし、Threadというクリティカルなリソースを消費するため、システムが不安定になるリスクを抱えることになります。

という特徴を持つ2つのプールを必要に応じて選択すれば、99割のケースには対応できるのでは無いかと思います。

なお、Scalaがデフォルトで提供している scala.concurrent.ExecutionContext.Implicits.global は、newFixedThreadPool()にCPUコア数を渡して生成するようなので、ちょっとThread数がプア過ぎます(設定で変えられるらしい)。

システムの用途に合わせて ExecutionContext を切り替えたほうが良いですね。

ソースコード

と、ここまでコードレベルでの説明を一切してこなかったのですが、今回の話を検証したサンプルコードをgithubに乗せておきましたので、必要に応じて確認してください(と手抜き)。

GitHub - qtamaki/scala-executioncontext-example

追記: ForkJoinPool

ExecutionContext の実行アルゴリズムとなりうる ExecutorService を実装したクラスは ThreadPoolExecutor 以外にも、(標準ライブラリ上に)幾つかあります。

そのうちの一つが ForkJoinPool です。

ForkJoinPool は、ThreadPoolExecutor と似たThreadプールを提供しますが、そのアルゴリズムは似て非なるものです。

簡単に言うと、ThreadPoolExecutor は1本のタスクキューとThreadプールを持ち、タスクを終えたThreadに順次キューからスレッドを割り当てていきますが、 ForkJoinPool では、ThreadプールのThread毎にキューを持っており、個別にキューを消費していきます。個別のキューの消費時には、コンテクストスイッチングが発生しないため、高速に動作する可能性があります。

Fork/Join Frameworkの性能について(PDF)

この辺の資料を読む限りだと、タスクの発生とプールのThreadの数によってパフォーマンスにバラツキがあり、最適解を求めるのであればチューニングが必須で、考えなしに使うとシングルスレッドの場合より処理効率がだいぶ落ちる事があるようです。

なお、使用の際のパラメータは、Threadプールの数を指定するのみなので簡単です。

処理に応じて最適な数を渡してあげると良いと思います(言うは易し)。