Scalaで副作用の無いスレッドセーフな設計
今回の記事を担当する新卒の西村と申します、よろしくお願いします。 今回のテーマはオブジェクト指向に関数型の特徴を取り入れることで、副作用の無いスレッドセーフな設計をしようと言うものです。 Javaではスレッドセーフな設計をすることがひとつの壁であるかのように感じられますが、 関数型のように書いていくことでスレッドセーフがぐっと身近になるということを紹介したいと思います。
対象読者
- Java利用者
- Scalaに興味がある人
- Javaで並行処理プログラムを書いたことのある人
実行環境
- Scala2.9.1
- JDK 1.7.0_01
Scalaを使うメリット
最近の言語の中でScalaは関数型言語とオブジェクト指向の特徴を両方備えた言語として注目されています。 先日2011/12/10に第2回Scala会議が行われ、ニコニコ生放送やUstreamで生放送されました。 ドワンゴからもたくさんの人が参加して発表を行っており、Scalaへの関心が高まってきていると感じます。 またScala会議の中で日本のScalaコミュニティとしてScalaJPが発足するなど、Scalaの今後の進展も注目されます。
Scalaはオブジェクト指向の利点と関数型の利点を持っており、容易にスレッドセーフな設計を実現できます。 また、JVM環境の上で動作する言語であることから、Javaのライブラリを利用したりJavaが動作する様々な環境で利用することのできる言語です。
一般にJavaでスレッドセーフな設計をすることは難しいと思われています。 その原因のひとつとして考えられるのが「副作用」を伴う操作を基本としているという点です。 一方、関数型で書かれたコードは「副作用」を無くすことで並行処理に強いという側面を持っており、 スレッドセーフな設計というものはさほど難しいことではありません。
今回はスレッドセーフな設計をする方法として、関数型の特徴に少し触れてみましょう。
副作用
先程の中で「副作用」という単語が出てきました。 副作用とは状態の変更を引き起こす操作や、それによって影響を受ける操作のことを指します。 例えばJavaのStringBufferクラスのもつappendメソッドは副作用を持った操作です。
StringBuffer sb = new StringBuffer("hoge");
System.out.println(sb); // 出力結果:hoge
sb.append("fuga"); // 文字列の追加(内部状態の変化)
System.out.println(sb); // 出力結果:hogefuga
このようにメソッドの呼び出しによってsb内部の状態が変化しています。 副作用を伴う操作は有用な場面も多くありますが、マルチスレッドで利用した際にバグの原因となることがあります。 例えば上記StringBufferの操作のような「追加してから表示」といった操作を複数のスレッドから実行した場合に、 追加後の文字列が意図した文字列とは限らないといったことがありえます。図にすると以下のようなイメージです。
シングルスレッドの場合自身がappendした結果の文字列が取得できますが、 マルチスレッドの場合、追加して確認するまでの間に別のスレッドのappendが入る可能性があります。
StringBufferはスレッドセーフなクラスとして設計されたものですが クラスを扱う側が正しく扱わなければ意味がありません。 副作用を前提とした操作は便利ではありますがマルチスレッドで扱う場合には慎重に利用する必要があります。
一方、StringクラスのtoUpperCaseメソッドは副作用を持たない操作です。
String str = new String("hoge");
System.out.println(str); // 出力:hoge
String str2 = str.toUpperCase();// 大文字の新しいオブジェクトを返す(状態変化無し)
System.out.println(str2); // 出力:HOGE
System.out.println(str); // 出力:hoge
Stringクラスは同じメソッド呼び出し方を行えば同じ結果を返し、他から状態を変更されることの無い設計になっています。 このような設計になっているクラスは複数スレッドからの呼び出しに対しても同じ値を返すため、当然スレッドセーフとなります。
副作用を減らすために
Scalaは関数型言語の特徴を取り入れており、副作用の無い設計にするための方法を提供しています。 具体的には以下のような方法により「副作用のある操作」と「副作用の無い操作」を明確にしています。
- valとvar
- immutableコレクションとmutableコレクション
変数の定義
Scalaには変数を宣言するときの方法として「val」「var」の2つの方法があります。
- val 再代入が不可能な変数
- var 再代入が可能な変数
関数型の記法でプログラムを書いていくならなるべくvalを利用します。 これはJavaのfinal宣言と非常に似ており、valで定義された変数は一度代入したら変更されることがありません。
scala> val n = 10 // val変数の宣言
n: Int = 10
scala> n = 20 // 再代入(1度しか代入できない)
<console>:8: error: reassignment to val
n = 20
^
値が変化しないのでコードの可読性の向上や、初期化安全性といった問題にも強くなります。 まずはvalを意識して使っていくことが副作用の無い設計を目指す重要なポイントです。
varはJavaなどで利用してきた変数と同じ使い方をします。 valでは難しい処理もvarを使うことで簡潔なコードを書けたり、効率の良い処理を実現できたりします。
scala> var m = 10 // var変数の宣言
m: Int = 10
scala> m = 20 // 別の変数を代入
m: Int = 20
しかしvarは副作用をもった処理になりやすく、そういったものはマルチスレッドで問題になることがあります。 副作用をもった処理でスレッドセーフを実現するためにはロックなどで守る必要があります。 ロックを使った処理は、ロック開放のタイミング、デッドロック問題、ロックによる待ち時間の発生など様々な問題を引き起します。
Scalaは関数型とオブジェクト指向型の2つの側面を持っているため、valとvarの2つの選択肢があります。 valにはvalの良さ、varにはvarの良さがあることを理解した上でvalを選択していくことがスレッドセーフな設計への近道になります。
リストを扱ったプログラミング
関数型言語ではListというデータ構造が大活躍します。
これはJavaで言うコレクションクラスにあたります。Javaのコレクションはマルチスレッドで利用することを考えると貧弱な部分があります。 例えばAPIには「この実装は同期化されません」といった説明や「例外をスローします」といった説明があちらこちらにあります。
その点、Scalaのコレクションは関数型言語であるための機能を備えています。 そしてScalaのコレクションも「val」と「var」のように2種類に分れています。
immutableコレクションとmutableコレクション
Scalaのコレクションは2種類のパッケージに分かれています。 ひとつが「scala.collection.immutable」、もうひとつが「scala.collection.mutable」です。
- immutableの特徴
- 一度生成したら要素の変更はできない(immutableは不変という意味)
- 要素に対して操作を行うと新たな要素を作成して返す。(永続データ構造)
- 副作用なし
- mutableの特徴
- 生成した後から要素の変更ができる(mutableは可変という意味)
- 破壊的なメソッドの利用が可能
- 副作用あり
Scalaでは選択肢があるので、場合に応じて使い分けていくことができます。 今回のようにスレッドセーフを意識した処理を書く場合にはimmutableコレクションの利用が最適です。 immutableコレクションは副作用を持ったメソッドがありません。
ではまず副作用のあるmutableコレクションの例から見ていきましょう。
scala> import scala.collection.mutable
import scala.collection.mutable
scala> val m = mutable.Map(1->"hoge", 2->"fuga", 3->"piyo", 4->"moge")
m: scala.collection.mutable.Map[Int,java.lang.String] = Map(3 -> piyo, 4 -> moge, 1 -> hoge, 2 -> fuga)
scala> m.update(1, "hogehoge") // 値の更新
scala> print(m)
Map(3 -> piyo, 4 -> moge, 1 -> hogehoge, 2 -> fuga)
scala> m.clear() // 要素をすべて削除
scala> print(m)
Map()
このようにmutableコレクションでは副作用を持ったメソッドの利用が可能です。 しかしmutableコレクションはJavaのコレクションよりもよく出来ている点があります。
scala> import scala.collection.mutable
import scala.collection.mutable
scala> val m = mutable.Map(1->"hoge", 2->"fuga", 3->"piyo", 4->"moge")
m: scala.collection.mutable.Map[Int,java.lang.String] = Map(3 -> piyo, 4 -> moge, 1 -> hoge, 2 -> fuga)
scala> val n = m.updated(1, "hogehoge") // updatedメソッド(副作用無し)
n: scala.collection.mutable.Map[Int,java.lang.String] = Map(3 -> piyo, 1 -> hogehoge, 4 -> moge, 2 -> fuga)
scala> print(n)
Map(3 -> piyo, 1 -> hogehoge, 4 -> moge, 2 -> fuga)
scala> print(m)
Map(3 -> piyo, 4 -> moge, 1 -> hoge, 2 -> fuga)
このようにmutableコレクションは状態を変化させない操作も備えています。 mutableコレクションを使うにしてもなるべく状態を変化させない方法を採用しましょう。
ではimmutableコレクションではどうなっているか確認してみます。
scala> val m = Map(1->"hoge", 2->"fuga", 3->"piyo", 4->"moge")
m: scala.collection.immutable.Map[Int,java.lang.String] = Map(1 -> hoge, 2 -> fuga, 3 -> piyo, 4 -> moge)
scala> m.update(1, "hogehoge")
<console>:9: error: value update is not a member of scala.collection.immutable.Map[Int,java.lang.String]
m.update(1, "hogehoge")
^
scala> m.clear()
<console>:9: error: value clear is not a member of scala.collection.immutable.Map[Int,java.lang.String]
m.clear()
^
このようにそもそも破壊的なメソッドが存在しません。 気づいた方もいるかもしれませんが、mutableコレクションを利用するためには「scala.collection.mutable」と importしなくては行けないのに対し、immutableコレクションでは何もimportしていません。 つまりScalaが標準でimportしているコレクションはimmutableです。
「val」と「immutableコレクション」を意識して利用することでスレッドセーフな設計はとても身近なものになるはずです。
スレッドセーフなクラス設計
では実際のプログラムを例にスレッドセーフなプログラムを書いて見ましょう。 作成クラスはHTMLファイルの要素を格納したListを保持し、要素への操作を提供しているクラスです。
Javaのコード例(スレッドセーフではない)
// 注意:メソッドはいいかげんな実装です。
import java.util.List;
import java.util.ArrayList;
public class HTMLObject{
private List<String> html;
public HTMLObject(List<String> html){
this.html = html;
}
// 新しいHTML要素のセット
public void setHTML(List<String> html){
this.html = html;
}
// htmlを返す
public List<String> getHTML(){
return this.html;
}
// 引数で指定されたタグの数を返す
public int countTag(String tagName){
int count = 0;
for(String tag : html){
if(tagName.equals(tag)){
count++;
}
}
return count;
}
//一致したタグを持つ要素を削除
public void removeTag(String tagName){
List<String> list = new ArrayList<>();
list.add(tagName);
html.remove(tagName);
}
//一致した要素をリストで返す
public List<String> getTagElements(String tagName){
ArrayList<String> tagList = new ArrayList<String>();
for(String tag : html){
if(tagName.equals(tag)){
tagList.add(tag);
}
}
return tagList;
}
}
コードが長くなるためいい加減な実装ですが、このコードの問題点はたくさんあります。
まず引数がList
CopyOnWriteArrayList<String> safeList = new CopyOnWriteArrayList<String>();
ArrayList<String> unsafeList = new ArrayList<String>();
HTMLObject tag1 = new HTMLObject(safeList); // OK
HTMLObject tag2 = new HTMLObject(unsafeList); // OK
スレッドセーフなCopyOnWriteArrayListオブジェクトを渡してくれるなら良いのですが、 もしかするとArrayListが渡されるかもしれません。その場合は例外により処理が中断する可能性があります。 この実装のままスレッドセーフなListを利用したいのであれば以下のようにします。
public class HTMLObject<T extends CopyOnWriteArrayList<String>>{
private T html;
public HTMLObject(T html){
this.html = html;
}
public void setHTML(T html){
this.html = html;
}
...............省略............
CopyOnWriteArrayListは操作の内部で要素のコピーをして変更をしています。そのため複数スレッドから状態を変更されても例外を出しません。 例外を出さない点を除いてArrayListと同等の利用ができるmutableなコレクションです。
removeTagメソッドのように要素の削除といった状態を変化させるメソッドは副作用を持った操作です。 複数のスレッドから「getTagElements」と「removeTag」を同時に実行した場合などは動作の予想が付きません。 副作用に依存している処理は、マルチスレッドにした途端に破綻する危険性があります。 ではこのHTMLObjectクラスをimmutableなクラスに書き変えてみましょう。
改良したJavaコード例
import java.util.List;
import java.util.ArrayList;
//immutableクラス
public class HTMLObject{
private final List<String> html;
public HTMLObject(List<String> html){
this.html = new ArrayList<String>(html); // コピー
}
public List<String> getHTML(){
return new ArrayList<String>(html); // コピー
}
// 引数で指定されたタグの数を返す
public int countTag(String tagName){
int count = 0;
for(String tag : html){
if(tagName.equals(tag)){
count++;
}
}
return count;
}
// 一致したタグを持つ要素をすべて削除
public HTMLObject removeTag(String tagName){
List<String> copyList = new ArrayList<String>(html); // コピー
copyList.remove(tagName);
return new HTMLObject(copyList); // 新しくオブジェクトを作成
}
// 一致した要素をリストで返す。
public List<String> getTagElements(String tagName){
ArrayList<String> tagList = new ArrayList<String>();
for(String tag : html){
if(tagName.equals(tag)){
tagList.add(tag);
}
}
return tagList;
}
}
このクラスは状態を変化させるメソッド呼び出しに対してコピーを作成し、コピーに対して操作を行っています。 副作用を持つクラスであったとしても、このように状態の不変を守ることでimmutableなクラスを実現することができます。 コンストラクタに渡されたコレクションへの参照は外部から変更される危険があるためコピーして新しくオブジェクトを生成しています。
では実際に利用して実行結果を確認してみます。
object Main{
def main(args:Array[String])={
List<String> list = new ArrayList<String>(); // リストの準備
list.add("<html>");list.add("<div>");
list.add("<div>");list.add("<br>");
HTMLObject obj = new HTMLObject(list);
System.out.println(obj.getHTML()); // [<html>, <div>, <div>, <br>]
List<String> result = obj.removeTag("<div>").removeTag("<br>").getHTML();// 要素の削除
System.out.println(result); // [<html>]
System.out.println(obj.getHTML()); // [<html>, <div>, <div>, <br>]
}
}
内部の状態が変更されないimmutableな設計になっていることが解ります。 ただしコンストラクタへ渡されたコレクションが、コピーする前に外部から変更されるといったことも考えられるため、絶対安全ではありません。
Javaはインターフェースに定義されているメソッドが副作用を前提としていることが多く、コピーするなどして守ってやる必要があります。 このようにJavaでimmutableな設計をするためには手間がかかります。 ではScalaでHTMLObjectクラスを作成してみましょう。
Scalaのコード例
class HTMLObject(val html:List[String]){
def getHTML = html;
def setHTML(list:List[String]) = new HTMLObject(list)
def countTag(tagName:String)={
val reg = tagName.r //正規表現オブジェクトの作成
html.map(x => reg.findAllIn(x).size).sum
}
def removeTag(tagName:String)={
val reg = tagName.r
new HTMLObject(html.filter(x => reg.findFirstIn(x).isEmpty))
}
def getTagElements(tagName:String)={
val reg = tagName.r
html.filterNot(x => reg.findFirstIn(x).isEmpty)
}
}
このコードは役に立つようなものではありませんが、実際に動作させ使うことができます。 (Scalaでこういったリスト操作をするだけならクラスの必要すら無いほど簡単です) 実行結果は以下のようになります。
object Main{
def main(args:Array[String])={
val m = new HTMLObject(List("<html>","<br>","<input>","<table>","<p>"))
val c = m.countTag("<html>")
println(c); // 1
val n = m.removeTag("<html>")
println(n.getHTML) // List(<br>, <input>, <table>, <p>)
val elements = m.getTagElements("<html>")
println(elements); // List(<html>)
}
}
このScalaで書いたものは副作用の無い設計になっています。 内部で保持するhtml変数はコンストラクタの中でvalとして定義し、 引数に渡されているListはimmutableなリストです。そのため状態を変更する方法がありません。 最初にコンストラクタで値を与えたら、後はすべて引数に対応した値を返すだけです。 内部の状態を変化させるメソッドに関しては新しくインスタンスを生成し返しています。
マルチスレッドで利用してみる
スレッドセーフなクラスの設計も終りました。最後に、作成したJavaとScalaのプログラムを マルチスレッドで動作させて結果を確認して終りましょう。
結果を確認する方法にはScalaで書いたプログラムで実行することにします。
HTMLObjectクラスを利用するクラス
// テストクラス
class TestRunnable(val htmlObj:HTMLObject) extends Runnable{
def run() ={
var i = 0
// 10000回実行する
while(i < 10000){
htmlObj.countTag("<html>")
htmlObj.removeTag("<html>")
htmlObj.getTagElements("<html>")
i += 1
}
println(htmlObj.getHTML)
}
}
上記プログラムで作成したHTMLObjectクラスを実行してみましょう。 複数のスレッドが操作をした結果、内部の値が変化しないことを確認します。
ではまずJavaのHTMLObjectクラスの動作確認をしてみます。
import java.util.ArrayList //ArrayListのインポート
val list = new ArrayList[String]();
list.add("<html>");list.add("<div>");
list.add("<div>");list.add("<br>");
new Thread(new TestRunnable(htmlObj)).start();
new Thread(new TestRunnable(htmlObj)).start();
new Thread(new TestRunnable(htmlObj)).start();
new Thread(new TestRunnable(htmlObj)).start();
new Thread(new TestRunnable(htmlObj)).start();
実行結果は以下のようになります。
$ scala Main
[<html>, <div>, <div>, <br>]
[<html>, <div>, <div>, <br>]
[<html>, <div>, <div>, <br>]
[<html>, <div>, <div>, <br>]
[<html>, <div>, <div>, <br>]
JavaのHTMLObjectはすべてコピーに対して操作を行っているため内部の値が変化しません。
次にScalaのクラスを実行してみます。
val list = List("<html>","<div>","<div>","<br>")
val htmlObj = new HTMLObject(list)
new Thread(new TestRunnable(htmlObj)).start();
new Thread(new TestRunnable(htmlObj)).start();
new Thread(new TestRunnable(htmlObj)).start();
new Thread(new TestRunnable(htmlObj)).start();
new Thread(new TestRunnable(htmlObj)).start();
実行結果は以下のようになります。
$ scala Main
List(<html>, <div>, <div>, <br>)
List(<html>, <div>, <div>, <br>)
List(<html>, <div>, <div>, <br>)
List(<html>, <div>, <div>, <br>)
List(<html>, <div>, <div>, <br>)
値が変化していませんね。 このようにimmutableな設計になっているクラスはマルチスレッド利用でも安心して参照を渡すことができます。
まとめ
Javaはimmutableでの利用を想定した設計となっていないため、 immutableを実現するにはmutableな操作を上手くコントロールしてやる必要があります。 その点、関数型での利用を想定した設計となっているScalaは副作用の無いimmutableな設計を容易に実現することができます。
最後に
今回紹介した記事はスレッドセーフを実現するための手法のひとつです。 マルチスレッドを扱うには高度な知識と経験を必要としますが、関数型の特徴を取り入れたScalaはそれらの問題を簡単にしてくれます。
今後はマルチコア、マルチスレッドで動作するプログラムを書く機会がどんどん増え、 スレッドセーフは避けて通れない問題になってくるかもしれません。 今からでもマルチスレッド利用を意識したプログラムを書くようにしていきたいですね。
著者: