Java.next: Clojure での並行処理

Clojure が並行処理と共有状態を抽象化する方法

すべての Java.next 言語のなかで、Clojure は並行処理に関して最も急進的な手法を採用しています。この記事では、Epochal Time Model、ソフトウェア・トランザクション・メモリーをはじめとする、Clojure の並行処理に関するさまざまな側面のうちのいくつかを取り上げて、詳細を探ります。

Neal Ford, Director / Software Architect / Meme Wrangler, ThoughtWorks

Photo of Neal fordNeal Ford は世界的な IT コンサルティング企業 ThoughtWorks のディレクターであり、ソフトウェア・アーキテクトであり、Meme Wrangler でもあります。また彼は、アプリケーション、教育資料、雑誌記事、コースウェア、ビデオや DVD によるプレゼンテーションなどの設計と開発も行っています。さまざまな技術に関する本の著者、編集者でもあり、最新の著作は『Presentation Patterns』です。彼は大規模なエンタープライズ・アプリケーションの設計や構築を専門にしています。また彼は世界各地で開催される開発者会議での講演者としても国際的に有名です。彼の Web サイトをご覧ください。



2014年 6月 19日

この連載について

Java の遺産となるのは、プラットフォームであって、言語ではないでしょう。200 を超える言語が JVM 上で実行されている今、最終的にこれらの言語の 1 つが JVM のプログラミングに最適な方法として Java 言語に取って代わることは避けられません。この連載では、Java 開発者が自分たちの近い将来を垣間見ることができるように、3 つの次世代 JVM 言語 ― Groovy、Scala、Clojure ― について、新しい機能やパラダイムを比較対照することで、詳しく探ります。

すべての Java.next 言語のうち、Clojure の並行処理メカニズムおよび機能は最も急進的です。Groovy と Scala は、改善された抽象化と構文糖を組み合わせて並行処理に対処している一方、Clojure は不変性に対するその強硬的な姿勢を活かして、JVM 上で他に例のない動作を見せています。連載「Java.next」の今回の記事では、Clojure に備わる多数の並行処理の方法の中からいくつかを取り上げて説明します。最初に取り上げるのは、Epochal Time Model という、Clojure での可変参照の基礎となっている根本的な抽象化です。

Epochal Time Model

Clojure が他の言語から大きくかけ離れているのは、可変の状態と値にその根本的な理由があると思います。Clojure での値には、対象とするありとあらゆるデータを設定することができます。Clojure では、値が数字の 42 になることや、{:first-name "Neal :last-name "Ford"} というマップ構造になること、さらにはウィキペティアなどの大規模なデータ構造になることもあります。基本的に、Clojure 言語はすべての値を、他の言語が数値を扱うのと同じように扱います。数字 42 は 1 つの値であり、これを定義し直すことはできません。けれども、この値に関数を適用して、別の値を返すことはできます。例えば、(inc 42) で値 43 を返すなどです。

Java や他の C ベースの言語では、変数が ID と値の両方を保持します。これが、Java 言語での並行処理を難しくしている要因の 1 つです。Java 言語の設計者たちが変数の抽象化を作成したのは、スレッド化を抽象化する前のことであり、変数の設計には並行処理での付加的な複雑さが考慮されていませんでした。Java の変数は単一スレッドを前提とすることから、マルチスレッド環境では変数を保護するために、同期化ブロックなどといった面倒なメカニズムが必要になります。Clojure の設計者である Rich Hickey 氏は、Java の変数設計の不備を言い表すために、「complect」という古語を復活させました (「complect」は、「絡み合わせること」または「織り合わせること」として定義されます)。

一方、Clojure は値を参照から切り離します。Clojure の世界観では、データは一連の不変の値として存在します (図 1 を参照)。

図 1. Epochal Time Model での値
Epochal Time Model での値を示す図

図 1 では、v1 などの個別の値が 42 やウィキペティアなどのデータを表すボックスとしてモデル化されていることを示しています。値とは独立している関数が、これらの値を引数として取り、新しい値を生成します (図 2 を参照)。

図 2. Epochal Time Model での関数
Epochal Time Model での関数を示す図

図 2 に円として示されている関数は、値とは独立しています。値を引数および結果として使用した関数呼び出しによって、新しい値が生成されます。一連の値は、変数の ID を表す参照に保持されます。図 3 の点線枠で示されているように、この ID は時が経つにつれて異なる値を指すことになりますが (関数が適用されるため)、ID が変わることはありません。

図 3. Epochal Time Model での参照
Epochal Time Model での参照を示す図

図 3 のイメージ全体が、単一の参照の経時的な変化を表しています。点線枠で表されている参照は、その存続期間全体にわたり、一連の値を保持します。参照には、ある時点で新しい不変の値を割り当てることができます。つまり、参照を変更することなく、参照が指す内容を変更できるというわけです。

参照の存続期間中は、図 4 に示すように 1 つ以上のオブザーバー (他のプログラム、ユーザー・インターフェース、あるいは参照が保持する値の対象の部分) が参照を逆参照して値を確認します (そして、おそらくは何らかの形で値に作用します)。

図 4. 逆参照
逆参照を示す図。

図 4 のオブザーバー (2 つの扇形で示されています) は、参照そのものを保持 (点線枠で表されている参照から始まる矢印で表現してあります) することも、参照を逆参照して値を取得 (値から直接始まる矢印で表現してあります) することもできます。例えば、データベース接続を利用する関数を引数として受け取って、それを下位レベルのパーシスタンス関数に渡すとします。この場合、参照を保持しますが、参照の値は必要ありません。パーシスタンス関数が逆参照を行ってデータベースに接続するための値を取り出すことになります。

図 4 のオブザーバーの間では調整が行われないことに注意してください。オブザーバー間には依存関係がないためです。この構造により、Clojure ランタイムはいくつかの有用な特性 (リーダーに決してブロックさせないなど) を言語全体で保証できるため、極めて効率的に読み取り処理を行うことができます。参照を変更する必要がある場合には (つまり、参照が別の値を指すようにする場合)、Epochal Time Model を適用する Clojure の API のいずれかを使用して参照を更新します。

Clojure では一貫して、Epochal Time Model が参照更新の基礎となります。このモデルではランタイムがすべての更新を管理することから、ランタイムは Clojure ほど高度ではない他の言語において開発者が対処しなければならないスレッド化による競合を防ぐことができます。

Clojure では、どのような特性が必要であるかによって、さまざまな方法で参照を更新できるようになっています。次はそれらの方法のうち、単純な「アトム」と、高度な「ソフトウェア・トランザクション・メモリー」について説明します。

アトム

Clojure のアトムとは、アトミックなデータへの参照のことです (アトミックなデータであれば、その大きさは関係ありません)。atom を作成して初期化した後、その参照を変更するための関数を適用します。以下のコードでは、counter という名前の参照を作成し、0 に初期化されたアトムを参照します。参照を更新して新しい値を参照するようにするには、例えば (swap! ) 関数を使用して、参照の新しい値をアトミックに交換します。

(def counter  (atom 0))
(swap! counter + 10)

Clojure での慣例により、変更を適用するための関数の名前の最後には感嘆符が付きます。(swap! ) 関数が引数として取るのは、参照、適用する関数 (この例の場合、+ 演算子)、そして追加の引数です。

Clojure のアトムは、プリミティブ型の値だけでなく、任意のサイズのデータも保持します。例えば、person マップを中心としたアトミック参照を作成し、それを map 関数で更新することができます。以下のコードは、アトム内で (create-person) 関数を使用して (この関数の詳細は示してありません) person レコードを作成した後、(swap! ) と、マップの関連情報を更新する (assoc ) を使用して参照を更新します。

(def person (atom (create-person)))
(swap! person assoc :name "John")

アトムは、アトムでの共通オプティミスティック・ロック・パターンも実装します。それには、以下のように (compare-and-set! ) 関数を使用します。

(compare-and-set! a 0 42)
=> false

(compare-and-set! a 1 7)
= true

(compare-and-set! ) 関数が引数に取るのは、アトム参照、期待される既存の値、新しい値の 3 つです。アトムの値が期待される値と一致しなければ、更新は行われません。その場合、関数は false を返します。

Clojure には、参照セマンティクスに従った多種多様なメカニズムがあります。例えば、別のタイプの参照であるプロミス (promise) は、値を後から提供することを約束します。以下のコードでは、number-later という名前のプロミスへの参照を作成しています。このコードは値を生成することなく、最終的に実行するプロミスだけを生成します。(deliver ) 関数が呼び出された時点で、値が number-later にバインドされます。

(def number-later (promise))
(deliver number-later 42)

この例では Clojure の futures ライブラリーを使用していますが、参照セマンティクスは、単純なアトムと整合しています。

ソフトウェア・トランザクション・メモリー

Clojure の機能のなかで、ソフトウェア・トランザクション・メモリー (STM) ほど注目を浴びている機能は他にありません。STM とは、Java 言語がガーベッジ・コレクションをカプセル化するような方法で、Clojure が並行処理をカプセル化することを可能にする内部メカニズムです。つまり、同期化ブロック、デッドロック、スレッド・ライブラリーなどを一切考えずに、ハイパフォーマンス・マルチスレッド Clojure アプリケーションを作成することができます。

Clojure は、参照のあらゆる変更を STM を通して制御するという方法で、並行処理をカプセル化します。参照 (唯一の可変の抽象化) を更新するときには、Clojure ランタイムが更新を管理できるように、トランザクション内で参照を更新する必要があります。典型的な銀行業務の問題として、ある口座から引き落としを行うのと同時に、別の口座に振り込む場合を考えてみてください。リスト 1 に、単純な Clojure ソリューションを記載します。

リスト 1. 銀行業務のトランザクション
(defn transfer
  [from to amount]
  (dosync
   (alter from - amount)
   (alter to + amount)))

リスト 1 では、引数として from (口座から)、to (口座へ) ― どちらも参照 ―、そして amount (金額) という 3 つを取る (transfer ) 関数を定義しています。from 口座の金額を減算して、その分を to 口座に追加しますが、この処理は (dosync ) トランザクション内で行わなければなりません。トランザクション・ブロックの外部でいずれかの (alter ) 呼び出しを試行すると、その更新試行は以下の IllegalStateException で失敗します。

(alter from - 1)
=>> IllegalStateException No transaction running

リスト 1(alter ) 関数も Epochal Time Model に従っていますが、この関数は STM を使用することで、2 つの処理が両方とも完了しない限り、どちらの処理も行わなかった状態になるようにしています。その目的のために、STM は (データベース・サーバーとまったく同じように) 一時的にブロックされる処理を再試行します。従って、更新を行う関数が更新以外の副次作用を及ぼさないようにすることが重要です。例えば、関数がログへの書き込みも行うとしたら、再試行の結果、複数のエントリーがログに記録されることになります。STM はまた、保留中のトランザクションの優先度をその経過時間に従って徐々に上げていき、データベース・エンジンで一般的に見られるような他の振る舞いも見せます。

STM は使いやすいものの、その基礎となっているのは高度な仕組みです。その名前のとおり、STM はトランザクション・システムであり、ACID トランザクション標準の ACI の部分を実装します。つまり、すべての変更はアトミック性 (A)、一貫性 (C)、独立性 (I) を確保した形で行われます。STM はインメモリーで処理を行うため、ACID の永続性 (D) の部分は適用されません。STM のようなハイパフォーマンス・メカニズムが言語のコアに組み込まれているのはまれなことです。Clojure 以外で本格的な STM を実装している主流の言語と言えば、Haskell しかありません。Haskell は Clojure と同じように不変性を重要視しているため、それも当然です (.NET エコシステムは STM マネージャーを作成しようとしましたが、トランザクションと可変性の処理があまりにも複雑になってきたため、結局は断念しました)。


reducers と数値分類子

並列処理の説明を締めくくるには、前の記事で取り上げた数値分類子の問題を実装する他の方法を説明しないわけにはいきません。リスト 2 に、並列処理を使用しないイディオムのようなバージョンを記載します。

リスト 2. Clojure の数値分類子
(defn classify [num]
  (let [facts (->> (range 1 (inc num))
           (filter #(= 0 (rem num %))))
   sum (reduce + facts)
      aliquot-sum (- sum num)]
         (cond
         (= aliquot-sum num) :perfect
         (> aliquot-sum num) :abundant
         (< aliquot-sum num) :deficient)))

リスト 2 の数値分類子のバージョンは、Clojure のキーワード (先行するコロンで示されます) を返す単一の関数にまとめられています。(let ) ブロックを使用すれば、ローカルなバインディングを設定することができます。約数を判別するために、thread-last 演算子を使用して数値の範囲を絞り込み、よりシーケンシャルなコードにしています。sumaliquot-sum の計算はどちらも単純で、ある数値の約数の和 (Aliquot sum) は、その数値の約数の和から数値自体を引いた値ということになります。これにより、この比較コードは単純なものになっています。関数の最後の行は、計算された値に対して aliquot-sum を評価して、適切なキーワードの列挙を返す (cond ) 文です。上記のコードで興味深い側面は、このバージョンでは、これまでの実装で使用したさまざまなメソッドを、単純な代入へと簡素化しているところです。計算を十分に単純で簡潔なものにすれば、形式上作成しなければならない関数の数が減ります。

Clojure には reducers と呼ばれる強力な並行処理ライブラリーが同梱されています (reducers がどのように開発されたかは、― 最新の JVM にネイティブな fork/join 機能を利用するための最適化を含め ― 非常に興味をそそられる物語です)。reducers ライブラリーは、mapfilterreduce などのよく使われる処理が複数のスレッドを自動的に利用できるようにするために、これらの処理の簡単な置き換えとなる処理を提供しています。例えば、標準の (map )(r/map ) に置き換えると (r/ はreducers の名前空間です)、ランタイムによって map 処理が自動的に並列化されます。

reducers を使用した数値分類子のバージョンをリスト 3 に記載します。

リスト 3. reducers ライブラリーを使用した数値分類子
(ns pperfect.core
  (:require [clojure.core.reducers :as r]))

(defn classify-with-reducer [num]
  (let [facts (->> (range 1 (inc num))
		   (r/filter #(= 0 (rem num %))))
	sum (r/reduce + facts)
	    aliquot-sum (- sum num)]
               (cond
               (= aliquot-sum num) :perfect
               (> aliquot-sum num) :abundant
               (< aliquot-sum num) :deficient)))

目を凝らして見なければ、リスト 2リスト 3 の違いは見つけられません。違っている点は、reducers の名前空間と別名が導入されていること、そして filter と reduce の両方に r/ が追加されていることだけです。このわずかな変更によって、filter 処理と reduce 処理の両方が、複数のスレッドを自動的に使用できるようになっています。


まとめ

今回の記事では、議論すべき内容が豊富な、Clojure での並行処理の方法をいくつか取り上げました。まずは、コアとなる基本的な抽象化 Epochal Time Model について説明し、アトムと STM でこの概念がどのように使用されているのかを明らかにしました。さらに、既存のアプリケーションで fork/join のような高度な並行処理機能を簡単に利用できるようにする、簡単なドロップイン・ライブラリーについても説明しました。

pmap (並列バージョンの map) などの単純な並列関数を含め、Clojure には他にも多数の並行処理の方法があります。また、Clojure はエージェントも組み込みます。エージェントとは (システム定義またはユーザー定義の) プール内のスレッドにバインドされる自律型ワーカーで、概して Scala のアクターに似ています。しかも、Clojure は Java 言語における最近の並行処理の進化をすべてカプセル化し、fork/join などの最近のライブラリーを簡単に使用できるようにしています。

おそらく Clojure の他のどの機能にも増して、並行処理機能は Clojure エコシステムの設計における焦点を明らかにしています。その焦点とは、言語機能をフルに活用して強力な抽象化を確立することです。Clojure は単に Java の Lisp のようなバージョンを作ろうとしているのではありません。Clojure の設計者たちは、コアのインフラストラクチャーおよび実装を根本から見直しています。

次回の記事では、Java.next 言語としての Java 8 を取り上げます。

参考文献

学ぶために

  • Clojure: Clojure は JVM 上で実行される最近の関数型 Lisp です。
  • Clojure と並行性」(Michael Galpin 著、developerWorks、2010年9月): 並行性に対する Clojure の各種手法について詳しく読んで、それぞれの手法がどのようなときに適切であるかを理解してください。
  • Stuart Halloway on Clojure」: Clojure コア・メンバーである Halloway への Andrew Glover によるインタビューをポッドキャストで詳しく聞いてください。
  • Reducers - A Library and Model for Collection Processing」: このホワイト・ペーパーを読んで、reducers ライブラリーに関するインスピレーション、設計、実装について学んでください。
  • Haskell: 学術界および実際のアプリケーションで使用されている純粋に関数型の言語である Haskell について調べてください。
  • 関数型の考え方」: Neal Ford による developerWorks での連載で、関数型プログラミングについて詳しく調べてください。
  • developerWorks Java technology ゾーン: Java プログラミングのあらゆる側面を網羅した豊富な記事を調べてください。

議論するために

  • developerWorks コミュニティーに参加してください。ここでは他の developerWorks ユーザーとのつながりを持てる他、開発者によるブログ、フォーラム、グループ、Wiki を調べることができます。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology, Open source
ArticleID=974432
ArticleTitle=Java.next: Clojure での並行処理
publish-date=06192014