この記事では、Clojure プログラミング言語とこの言語が持つ並行処理機能について詳しく見ていきます。これは Clojure の入門記事ではないため、読者が Clojure についてある程度の知識があることを前提とします。サンプル・コードを実行するには、Clojure 1.1 が必要です。したがって Java 1.5 以上が必要となります。この記事では Java 1.6.0_20 を使用しました。記事で使用するツールについては、「参考文献」のリンクを参照してください。記事のソース・コードは、「ダウンロード」セクションからダウンロードすることができます。
ここ数年の間、ソフトウェアの開発者たちは、並行プログラミングがデファクトのプログラミング手法となるだろうという噂を耳にしてきました。その主な理由は、コンピューター・プロセッサーの速度が横ばい状態になっている一方、コンピューターごとのプロセッサーの数は増えているからです。チップあたりのプロセッサー数が増加しているということは、これまでまさに、ムーアの法則が当てはまっていたということです。ムーアの法則については、ウィキペディアにわかりやすく要約されています (リンクについては「参考文献」を参照)。
「近頃は、ムーアの法則によってもたらされるメリットをフルに活用するために並列コンピューティングが欠かせなくなっています。長年にわたり、プロセッサー・メーカーはクロック速度とインストラクション・レベルでの並列性を伸ばし続けてきました。そのためプロセッサーが新しくなるたびに、まったく変更を加えていないシングル・スレッドのコードでも、その実行時間が短縮されてきましたが、現在、プロセッサー・メーカーはCPU の消費電流の管理を目的に、マルチコア・チップの設計を支持するようになっています。したがって、ハードウェアを十分に活用するためには、ソフトウェアがマルチスレッド方式、あるいはマルチプロセス方式で作成されていなければなりません。」
上記段落の内容は、この記事を読む大きなきっかけとなるかもしれませんが、このような説明は長年にわたって広く知られるようになっています。それでもなお、多くの開発者は進んでシングル・スレッドのコードを作成しています。その大きな理由の 1 つは、インターネットの支配です。新しく作成されるアプリケーションのかなりの数が Web アプリケーションです。そしてサーバー・サイドの Web アプリケーション開発は、主にシングル・スレッドのプログラミングです。Web サーバーはサーバーの多数のコアを利用してユーザーからの多数の同時リクエストに対応するものの、ユーザー・リクエストのそれぞれは、大抵はシングル・スレッドのコードで処理することができます。これは素晴らしいことであり、Web アプリケーションが成功している数多くの理由の 1 つでもあります。したがって、ユーザーのラップトップやデスクトップ・コンピューターにある多数のコアは、多くの開発者には関係しません。
Web の成功だけが、並行プログラミングに新たな技術が登場しない理由ではありません。実のところ、Web アプリケーション開発とその歴史を調べてみれば、開発者にとって Web 開発がいかに容易なものになっているかに気付かざるを得ないはずです。PHP や JSP、そして Ruby on Rails に至るまで、Web 開発は次第に簡易化され、それに伴い開発者はさらに驚くようなことを Web 上で実現できるようになっています。この事実を並行プログラミングと比較してみてください。よく使われているほとんどのプログラミング言語 (C++ や Java など) での並行プログラミングの構成概念 (スレッド、ロック) は何十年もの間、ほとんど変わっていません。並行プログラミングはこれまで常に困難であり、これからも困難であり続けることから、避けられているというわけです。並行プログラミングは、社内に 1 人か 2 人の第一人者がいて、難しいタスクは誰もがその人物に信頼して任せるといった分野の 1 つとなっています。
このような分野に、比較的新しいプログラミング言語が登場してきています。その優れた一例が、Clojure です。Clojure は下位レベルで並行性を組み込んでいるため、スレッドとロックを扱う必要はありません。代わりに単純な、問題の少ないモデルで作業することができます。Clojure ではシステムを突然停止させるデッドロックの発生をそれほど心配することなく、アプリケーション・ロジックに再び専念できるようになるというわけです。それでは早速、Clojure に組み込まれた並行性の構成要素を検討していきましょう。
前述のとおり、よく使われているプログラミング言語のほとんどは、極めて基本的な並行処理機能としてスレッドとロックを提供しています。例えば、Java 5 とJava 6 では並行性を目的とした多数の新しいユーティリティー API を導入しましたが、そのほとんどは、スレッド・プールや各種のロックなどといったスレッドおよびロック・ベースのユーティリティーであるか、並行性やパフォーマンスの特性が改善されたデータ構造であるかのいずれかでした。つまり、並行プログラムの根本的な設計方法は何も変更されていないため、今でも解決しなければならない問題は相変わらず同じで、そのソリューションも同じく不安定です。単に作成するボイラープレート・コードが減っただけにすぎません。
Clojure はあらゆる側面において根本的に異なります。Clojure にはお決まりのプリミティブ、スレッド、ロックはありません。代わりに、Clojure では全く異なる並行プログラミング・モデルを使用することになります。言うまでもなく、これらのモデルにはスレッドも、ロックも含まれていません。ここで言うモデルは 1 つだけではないことに注意してください。Clojure には 4 つの異なる並行処理モデルがあり、そのそれぞれが、スレッドとロックをベースとした抽象化であると見なすことができます。ここからは、この 4 つの並行処理モデルを順に見ていきます。最初に取り上げるのは最も単純なモデル、var です。
Clojure の並行処理モデルのなかで最も単純な var は、変数とその値の宣言でしかありません。リスト 1 に、Clojure で var を使用する場合の単純な例を記載します。
リスト 1. Clojure の var
1:1 user=> (defstruct item :title :current-price)
#'user/item
1:2 user=> (defstruct bid :user :amount)
#'user/bid
1:3 user=> (def history ())
#'user/history
1:4 user=> (def droid (struct item "Droid X" 0))
#'user/droid
1:5 user=> (defn place-offer [offer]
(binding [history (cons offer history)
droid (assoc droid :current-price (get offer :amount))]
(println droid history)))
#'user/place-offer
1:9 user=> (place-offer {:user "Anthony" :amount 10})
{:title Droid X, :current-price 10} ({:user Anthony, :amount 10})
nil
1:17 user=> (println droid) ;there should be no change
{:title Droid X, :current-price 0}
nil
|
リスト 1 では最初に item と bid という 2 つのデータ構造を宣言した上で、history という var を作成し、続いて droid という var を作成しています。前者は単なる空のリスト、後者は項目です。次に、place-offer という関数を作成しています。この関数は入札の付け値 (offer) を引数に取り、droid の current-price を変更してから、その入札の付け値を history に追加しています。この操作を行うために、binding マクロを使用していることに注目してください。このマクロが、var のスレッド・ローカルな値を変更します。したがって、place-offer 関数の実行スコープの中では droid と history が指す値はそれぞれ異なりますが、この関数の実行スコープ以外では 2 つの値は変更されません。Clojure では、デフォルトで何もかもが不変であることを思い出してください。var をバインディングすれば、簡単にスレッド・ローカルなスコープ内で値を変更できるようになります。スレッド・ローカルなスコープ内で変更される値は、その後に他のスレッドが読み取ったとしても、変更されていることはありません。このように、個別のタスクを実行する一環として状態を変化させる必要がある場合には、var を使うのが、その目的を達成する簡単な手段となります。一方、他のスレッドも認識するように状態を変更しなければならないとしたら、Clojure の atom を使用するほうが適しています。
atom は、その状態を変更することが可能な変数です。atom は至って簡単に使用することができ、完全に同期します。つまり、atom の値を変更する関数を呼び出した場合、その関数がリターンした時点で、すべてのスレッドが確実に、変更された新しい値を見ることになります。リスト 2 に atom の使用例を記載します。
リスト 2. Clojure の atom
1:21 user=> (def droid (atom (struct item "Droid X" 0)))
#'user/droid
1:22 user=> (def history (atom ()))
#'user/history
1:28 user=> (defn place-offer [offer]
(reset! droid (assoc @droid :current-price (get offer :amount))))
#'user/place-offer
1:33 user=> (place-offer {:user "Anthony" :amount 10})
{:title "Droid X", :current-price 10}
1:36 user=> (println @droid)
{:title Droid X, :current-price 10}
nil
|
上記のコードはリスト 1 の例をアレンジしたもので、今回は atom 関数を使用して droid と history を atom として定義しなおしています。atom 関数を使用することで、初期値をラップする atom オブジェクトを使えるようになります。新しい place-offer 関数では、reset! 関数を使って droid の値を変更します。ここで注意する点は、droid と history の前に @ 記号を追加していることです。こうすることによって、Clojure がポインターを逆参照して実際の値を提供することになります。続いて、この新しい place-offer 関数を呼び出すと、droid を出力して値が実際に変更されていることを確認できます。注意する点として、place-offer で変更した atom は droid だけで、history atom は変更しませんでした。もちろん reset! を使用して変更することはできますが、両方の変更が可視になるという保証はありません。別の言葉に置き換えると、スレッドによっては droid の変更後の値が見えても、history の変更後の値は見えない可能性があるということです。このような一貫性を確保するためには、調整が必要になります。つまり、トランザクションが必要です。ということは、ref が必要です。
ref は Clojure にその最強の並行性制御機構をもたらします。それが、Clojure のソフトウェア・トランザクション・メモリー (STM: Software Transactional Memory) 実装です。ref は atom と似ていますが、atom とは違って、大抵は追加しなければならないコードが 1 行だけで済みます。ref の大きな利点は、調整が行われることです。ref を使用すれば、複数のオブジェクトの状態を 1 つのトランザクション内で変更できるので、トランザクションはアトミック性、一貫性、独立性を持つことになります。つまり、ACID (Atomicity, Consistency, Isolation, Durability) の ACI の部分です (永続性 (Durability) がないのは、永続性はメモリーにのみ関連するからです)。独立性とは、オブザーバーがトランザクション内でのすべての変更を見るか、あるいは変更をまったく見ないかのどちらかであることを意味します。atom では、その限りではありません。リスト 3 に ref の使用例を記載します。
リスト 3. Clojure の ref
1:90 user=> (def droid (ref (struct item "Droid X" 0)))
#'user/droid
1:91 user=> (def history (ref ()))
#'user/history
1:92 user=> (defn place-offer [offer]
(dosync
(ref-set droid (assoc @droid :current-price (get offer :amount)))
(ref-set history (cons offer @history))
))
1:97 user=> (place-offer {:user "Tony" :amount 22})
({:user "Tony", :amount 22})
1:99 user=> (println @droid @history)
{:title Droid X, :current-price 22} ({:user Tony, :amount 22})
nil
|
リスト 3 のコードはリスト 2 のコードと非常によく似ています。これは、ref は atom と同じラッパー・パターンに従うためです。place-offer 関数の実装は dosync 関数の呼び出しで始まっています。この関数がトランザクションをラップして前述の調整を行うため、droid と history の両方を変更してもデータのダーティー読み取りが行われることはありません。atom での場合と同じく、この関数の実行後に値を逆参照して出力すると、値が変更されていることがわかります。
おそらく皆さんは、STM が一体どのような動作をするのか不思議に思っていることでしょう。あるスレッドが入札の付け値を 25 に設定して place-offer 関数を呼び出すと同時に、別のスレッドが同じ関数を付け値 22 で呼び出したとしたらどうなるかを考えてみてください。Clojure はトランザクションの途中で値が変更されないようにします。したがって、トランザクションが dosync ブロックの終わりに達した場合、このトランザクションが完了するより前に最新のトランザクションが開始されていることを STM が認識すると、最新のトランザクションがロールバックされて、再度実行されることになります。このように、関数は何度か実行される可能性があるため、トランザクションでは純粋関数 (副作用を持たない関数) だけを使用するということが極めて重要になります。Clojure はこのようなトランザクション/ロールバックを効率化するために、極めてパフォーマンスに優れた永続データ構造を使用します。
新しい入札の付け値が前の入札よりも高い場合にだけ、その新しい入札が選択されるようにするには、ref 宣言に検証用の関数を追加するだけでよいのです。すると、検証によってトランザクション中に変更が検出された場合、そのトランザクションはロールバックされて再び開始されます。検証のチェックに失敗すると、トランザクションは停止されます。
Clojure の STM を使用する際に重要なことは、dosync 関数の中にコードをラップすることです。観察力の鋭い人は、これは同期化したブロックの中、あるいはロック取得/解放フローの中にコードをラップすることとよく似ていると指摘するでしょう。もちろん、このような従来の並行性制御機構は、その難しさで知れ渡っていますが、Clojure はそれよりも単純です。状態を変更するつもりであれば、dosync を使用してください。dosync の外部で ref の状態を変更することはできません。さらに、Clojure のトランザクションは複合構成が可能なので、dosync ブロックの内側で、同じく dosync ブロックを持つ別の関数を呼び出すことができます。関数の間で共有されるロックのようなものを考え出す必要はなく、デッドロックを心配する必要もありません。ref と atom はどちらも同期関数です。状態の変化に同期する必要がないのであれば、agent を使用することで何らかのメリットがもたらされます。
状態を変更する必要があっても、変更されるまで待機する必要がない場合や、複数のスレッドによって変更が行われるときにその順序は問題にならない場合はよくあります。このよくあるパターンに対処するため、Clojure では 1 つのプログラミング・モデルを用意しています。それが agent です。リスト 4 に agent の使用例を記載します。
リスト 4. Clojure の agent
1:100 user=> (def history (agent ()))
#'user/history
1:101 user=> (def droid (agent (struct item "Droid X" 0)))
#'user/droid
nil
1:107 user=> (defn place-offer [offer]
(send droid #(assoc % :current-price (get offer :amount))))
1:110 user=> (place-offer {:user "Tony" :amount 33})
#<Agent@396477d9: {:title "Droid X", :current-price 0}>
1:111 user=> (await droid)
nil
1:112 user=> (println @droid)
{:title Droid X, :current-price 33}
nil
|
上記のコードも同じく、droid と history の初期値をラップするところから始まっていますが、そのために使用しているのは agent 関数です。続いて新しいバージョンの place-offer を定義しています。今度は、agent でラップされた値を直接変更することはできません。そこで使用するのが、send 関数です。この関数が引数として取るのは agent と関数です。2 番目の引数として取る関数が、agent の値に適用される関数です。この関数の実行結果の値が agent の値を置き換えるために使用されます。リスト 4 では、send に渡す関数として無名関数が使用されています。このように、関数を渡して状態の更新に使用するというプログラムの動作は、atom と ref でもサポートしていることに注意してください。次に使用しているのは、await 関数です。この関数は、agent が受け取った関数を実行し終わるまでスレッドをブロックします。これは、目的とする変更が実際に適用されたことを確実にするのに効果的な方法です。agent は非同期の性質を持つため、このようにしなければ、送信した関数が agent に適用されたかどうかが確実ではなくなるのです。
この記事では、Clojure の並行処理モデルのそれぞれを紹介しました。並行性の問題にはさまざまな種類がありますが、その多くは、Clojure のいずれかのモデルにぴったり対応します。Clojure の並行処理モデルを使用できれば、Clojure の機能を利用して遥かに簡単に問題を解決できるはずです。どの並行処理モデルも問題に適用できないとしたら、Clojure には Java との相互運用性があるので、その点を利用して Java のスレッドおよびロックを代わりに使用することもできます。以上の理由から、Clojure は、並行性に大きく依存するどんなタスクに対処する際にも常に念頭に置いておくべき言語であると言えます。
| 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|---|---|---|
| Article source code | auctions.clj.zip | 1KB | HTTP |
学ぶために
- ウィキペディアでムーアの法則について学んでください。
- 「Clojure プログラミング言語」(Michael Galpin 著、developerWorks、2009年9月): Clojure を使い始めるには、この記事を読んでください。
- clojure-contrib にアクセスして、Clojure コミュニティーによって作成され、多くの Clojure プロジェクトで使用されているこの必須ライブラリーを調べてください。このライブラリーは、デフォルトで Eclipse プラグインに組み込まれます。
- Clojure の初心者からエキスパートに上達する近道は、Stuart Halloway の『プログラミング Clojure』を読むことです。
- 「Beginning Haskell」(David Mertz 著、developerWorks、2001年12月): Clojure とは別の関数型言語について紹介しているチュートリアルです。
- developerWorks Web development ゾーンでは、多種多様な Web ベースのソリューションを話題にした記事を揃えています。
製品や技術を入手するために
- Clojure サイトにアクセスして Clojure をダウンロードしてください。このサイトには、チュートリアル、そして参考資料へのリンクも用意されています。
- Java SDK を入手してください。この記事では JDK 1.6.0_17 を使用しました。
- IBM 製品の評価版をダウンロードして、DB2®、Lotus®、Rational®、Tivoli®、および WebSphere® のアプリケーション開発ツールとミドルウェア製品を使ってみてください。
議論するために
- 今すぐ My developerWorks で自分のプロフィールを作って、Clojure に関するウォッチ・リストをセットアップしてください。My developerWorks とずっとつながっていられます。
- Web 開発に興味を持つ他の developerWorks メンバーを見つけてください。
- Web Development グループで、Web 開発者としての経験と知識を共有してください。
- Web のトピックを専門とする developerWorks グループに参加して、知識を共有してください。
- Roland Barcia が彼のブログで Web 2.0 とミドルウェアについて語っています。
- developerWorks のメンバーが共有する Web 関連のブックマークをフォローしてください。
- Web 2.0 Apps フォーラムで、素早く回答を得てください。

Michael Galpin は eBay のアーキテクトであり、developerWorks に頻繁に寄稿しています。彼は JavaOne、EclipseCon、AjaxWorld など、さまざまな技術カンファレンスで講演を行っています。彼が次に取り組もうとしていることを知るには、Twitter で @michaelg をフォローしてください。