Clojure と並行性

Clojure の 4 つの並行処理モデルについて学ぶ

最近、Clojure プログラミング言語に大きな注目が集まっています。しかし、この言語が注目されているのは、これが新しい Lisp の方言であること、あるいは Java™ 仮想マシン上で実行されることなどのあからさまな理由からではありません。多くの人々を引き付けている Clojure の魅力は、その並行処理機能です。Clojure はソフトウェア・トランザクション・メモリー (STM: Software Transactional Memory) モデルをネイティブにサポートする言語として最もよく知られていますが、あらゆる並行性の問題にとって常に STM が最適なソリューションとなるわけではないことから、Clojure には agent と atom という形で STM 以外のパラダイムのサポートも組み込まれています。この記事では、Clojure が提供する 4 つの並行処理手法について詳しく調べ、それぞれの手法が最適なソリューションとなる場合を探ります。

Michael Galpin, Software architect, eBay

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



2010年 9月 14日

前提条件

この記事では、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 に組み込まれた並行性の構成要素を検討していきましょう。


Clojure の並行処理手法

前述のとおり、よく使われているプログラミング言語のほとんどは、極めて基本的な並行処理機能としてスレッドとロックを提供しています。例えば、Java 5 とJava 6 では並行性を目的とした多数の新しいユーティリティー API を導入しましたが、そのほとんどは、スレッド・プールや各種のロックなどといったスレッドおよびロック・ベースのユーティリティーであるか、並行性やパフォーマンスの特性が改善されたデータ構造であるかのいずれかでした。つまり、並行プログラムの根本的な設計方法は何も変更されていないため、今でも解決しなければならない問題は相変わらず同じで、そのソリューションも同じく不安定です。単に作成するボイラープレート・コードが減っただけにすぎません。

Clojure はあらゆる側面において根本的に異なります。Clojure にはお決まりのプリミティブ、スレッド、ロックはありません。代わりに、Clojure では全く異なる並行プログラミング・モデルを使用することになります。言うまでもなく、これらのモデルにはスレッドも、ロックも含まれていません。ここで言うモデルは 1 つだけではないことに注意してください。Clojure には 4 つの異なる並行処理モデルがあり、そのそれぞれが、スレッドとロックをベースとした抽象化であると見なすことができます。ここからは、この 4 つの並行処理モデルを順に見ていきます。最初に取り上げるのは最も単純なモデル、var です。

スレッド・ローカルな 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 では最初に itembid という 2 つのデータ構造を宣言した上で、history という var を作成し、続いて droid という var を作成しています。前者は単なる空のリスト、後者は項目です。次に、place-offer という関数を作成しています。この関数は入札の付け値 (offer) を引数に取り、droidcurrent-price を変更してから、その入札の付け値を history に追加しています。この操作を行うために、binding マクロを使用していることに注目してください。このマクロが、var のスレッド・ローカルな値を変更します。したがって、place-offer 関数の実行スコープの中では droidhistory が指す値はそれぞれ異なりますが、この関数の実行スコープ以外では 2 つの値は変更されません。Clojure では、デフォルトで何もかもが不変であることを思い出してください。var をバインディングすれば、簡単にスレッド・ローカルなスコープ内で値を変更できるようになります。スレッド・ローカルなスコープ内で変更される値は、その後に他のスレッドが読み取ったとしても、変更されていることはありません。このように、個別のタスクを実行する一環として状態を変化させる必要がある場合には、var を使うのが、その目的を達成する簡単な手段となります。一方、他のスレッドも認識するように状態を変更しなければならないとしたら、Clojure の atom を使用するほうが適しています。

単純な同期のための 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 関数を使用して droidhistoryatom として定義しなおしています。atom 関数を使用することで、初期値をラップする atom オブジェクトを使えるようになります。新しい place-offer 関数では、reset! 関数を使って droid の値を変更します。ここで注意する点は、droidhistory の前に @ 記号を追加していることです。こうすることによって、Clojure がポインターを逆参照して実際の値を提供することになります。続いて、この新しい place-offer 関数を呼び出すと、droid を出力して値が実際に変更されていることを確認できます。注意する点として、place-offer で変更した atomdroid だけで、historyatom は変更しませんでした。もちろん reset! を使用して変更することはできますが、両方の変更が可視になるという保証はありません。別の言葉に置き換えると、スレッドによっては droid の変更後の値が見えても、history の変更後の値は見えない可能性があるということです。このような一貫性を確保するためには、調整が必要になります。つまり、トランザクションが必要です。ということは、ref が必要です。

トラザクションのための 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 関数の呼び出しで始まっています。この関数がトランザクションをラップして前述の調整を行うため、droidhistory の両方を変更してもデータのダーティー読み取りが行われることはありません。atom での場合と同じく、この関数の実行後に値を逆参照して出力すると、値が変更されていることがわかります。

おそらく皆さんは、STM が一体どのような動作をするのか不思議に思っていることでしょう。あるスレッドが入札の付け値を 25 に設定して place-offer 関数を呼び出すと同時に、別のスレッドが同じ関数を付け値 22 で呼び出したとしたらどうなるかを考えてみてください。Clojure はトランザクションの途中で値が変更されないようにします。したがって、トランザクションが dosync ブロックの終わりに達した場合、このトランザクションが完了するより前に最新のトランザクションが開始されていることを STM が認識すると、最新のトランザクションがロールバックされて、再度実行されることになります。このように、関数は何度か実行される可能性があるため、トランザクションでは純粋関数 (副作用を持たない関数) だけを使用するということが極めて重要になります。Clojure はこのようなトランザクション/ロールバックを効率化するために、極めてパフォーマンスに優れた永続データ構造を使用します。

新しい入札の付け値が前の入札よりも高い場合にだけ、その新しい入札が選択されるようにするには、ref 宣言に検証用の関数を追加するだけでよいのです。すると、検証によってトランザクション中に変更が検出された場合、そのトランザクションはロールバックされて再び開始されます。検証のチェックに失敗すると、トランザクションは停止されます。

Clojure の STM を使用する際に重要なことは、dosync 関数の中にコードをラップすることです。観察力の鋭い人は、これは同期化したブロックの中、あるいはロック取得/解放フローの中にコードをラップすることとよく似ていると指摘するでしょう。もちろん、このような従来の並行性制御機構は、その難しさで知れ渡っていますが、Clojure はそれよりも単純です。状態を変更するつもりであれば、dosync を使用してください。dosync の外部で ref の状態を変更することはできません。さらに、Clojure のトランザクションは複合構成が可能なので、dosync ブロックの内側で、同じく dosync ブロックを持つ別の関数を呼び出すことができます。関数の間で共有されるロックのようなものを考え出す必要はなく、デッドロックを心配する必要もありません。ref と atom はどちらも同期関数です。状態の変化に同期する必要がないのであれば、agent を使用することで何らかのメリットがもたらされます。

簡単な非同期のための 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

上記のコードも同じく、droidhistory の初期値をラップするところから始まっていますが、そのために使用しているのは 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 codeauctions.clj.zip1KB

参考文献

学ぶために

  • ウィキペディアでムーアの法則について学んでください。
  • 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® のアプリケーション開発ツールとミドルウェア製品を使ってみてください。

議論するために

コメント

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=Web development, Java technology
ArticleID=551212
ArticleTitle=Clojure と並行性
publish-date=09142010