並行処理してますか?(挨拶)
ScalaのWebフレームワークとして有名なPlay Frameworkには、標準でメッセージパッシング型並行処理ライブラリであるAkkaが添付されていて、Akkaのアクターを簡単に導入できるようになっています。 どんなものなのか、是非とも使ってみたいと思いませんか。思いますよね。思いましょう。
そういうわけで、Play FrameworkとAkkaを使って簡単なWebアプリケーションを作ってみましたので、解説する事にします。
注意
当記事内容につきまして、サンプル例がAkkaの用例としてはアンチパターンであるという旨のご指摘を頂きました。
「状態を持たない並行処理に対してAkka Actorを使うのはアンチパターン」という要旨です。これに関しまして次回の記事で、当記事のサンプルがアンチパターンである事の解説を書きたいと思います。
目次
対象読者
この記事は、「Play Frameworkで簡単な掲示板やTwitterクローンっぽいものを作った事はあるよ」という人達を対象にしています。
「名前くらいは聞いた事があるよ」「Play Frameworkなんて触った事もないよ」「むしろScala自体知らないよ」という方はまずLightbendさんのページからactivatorなりsbtなり落としてみて触ってみてください。
拙いコードですが、この記事を通してアクターモデルを利用した並列処理、イベント駆動な設計に興味を持っていただき、より「Playらしさ」「Scalaらしさ」のあるアプリケーションを作成するきっかけになれば、と思います。
何を作ったのか
フォームから検索条件を入力すると、その条件に従って、大量のデータから必要なものを抽出してくれるようなシステムを作ろうと思いました。
といっても、まず検索対象の「大量のデータ」を用意するのが大変ですね。 ランダムなデータを大量に投入してみてもいいですが、味気ないです。 またデータが大きくなると、自前でデータベースを管理するのも大変になってきます。 ちょっとPlay+Akkaを触ってみたいだけなのに、SQL文をたくさん書くのは嫌ですね。
そこで、無料で利用できるWebAPIを利用しました。
TwitterAPIとかでも良かったのですが、今回は個人利用なら登録不要で使える「国立国会図書館サーチAPI」を使ってみました。
何種類かAPIが用意されていますが、GETメソッドとリクエストパラメータで条件を指定でき、XMLで結果を取得できるOpenSearchを利用するのが良さそうです。 JSONで結果を返してくれるAPIが無いのが残念ですね……。
※利用にあたっては「APIのご利用について」を確認しましょう。
本記事では、Akka・アクターの利用に焦点を当てて解説していきます。
なお、コード全体はGithubで公開していますので御覧ください。
環境
バージョン | |
---|---|
MacOS | 10.11.2 |
Play Framework | 2.5.0-M2 |
裏話: 最初はWebSocketとかも使おうと思ったので、その辺りに色々変更の入った最新版の2.5系を入れたのですが、紙面の都合でなくなっちゃいました。せっかく(?)なのでPlayのバージョンはそのままです。
(何か2.5系に起因するエラーとかが起きて、それを解説できれば良いなぁ、と思ったのですが何も起きなかったです)
解説
アプリケーションの仕様は概ね以下の様なものです。
- ルート(/)にアクセスするとページを表示する
- タイトル、著者名、その他曖昧条件で資料を検索できる
- 最大取得数を1件から200件まで指定できる
ページ(View)はHTMLファイル1枚だけで遷移等は行わず、データ取得はAjaxで処理する事としました。
Controller
package controllers import akka.actor._ import akka.pattern.AskTimeoutException import com.google.inject.name.Named import com.google.inject.{Inject, Singleton} import play.api._ import play.api.data.Form import play.api.data.Forms._ import play.api.libs.concurrent.Execution.Implicits.defaultContext import play.api.libs.json.Json import play.api.mvc._ import scala.concurrent.Future import scalaz.\/.left import scalaz.{-\/, \/-} import actors.Library import actors.Library.{RequestBooks, BookData} import entities.Book import forms.SearchForm @Singleton class Application @Inject()(system: ActorSystem, @Named("ndl-client-actor") ndlClient: ActorRef) extends Controller { import akka.pattern.ask import scala.concurrent.duration._ def index = Action(Ok(views.html.index("Simple OPAC"))) val bookForm = Form( mapping( "title" -> optional(text), "author" -> optional(text), "any" -> optional(text), "count" -> optional(number(min = 1, max = 200)) )(SearchForm.apply)(SearchForm.unapply) ) implicit val bookToJson = Json.writes[Book] implicit val timeout: akka.util.Timeout = 1 minute lazy val libraryActor = system.actorOf(Library.props) def books = Action.async { implicit req => bookForm.bindFromRequest.fold( formWithError => { Future.successful(BadRequest("invalid request")) }, { case validForm => (try { libraryActor.ask(RequestBooks(validForm, ndlClient)).mapTo[BookData].map(_.books) } catch { case e: AskTimeoutException => Future.successful(left(s"Server Error: \n$e")) case _ => Future.successful(left("Something wrong...")) }).map { case \/-(books) => Ok(Json.toJson(books)) case -\/(msg) => InternalServerError(msg) } } ) } }
アクターの作成
このコード内では、アクターを2つの方法で作成し、利用しています。
ActorSystem#actorOf
メソッドで取得- Guiceを使ってDI
ActorSystem#actorOf
メソッドにPropsオブジェクトを与える事で、そのアクターを作成、取得する事ができます。
Propsオブジェクトというのは、Akkaアクターを初期化する為のオブジェクトで、基本的にAkkaのアクターはnewではなくPropsで取得する事になります。
ActorSystem#actorOf
メソッドには第二引数としてアクターの名前を与える事ができます。与えなければ自動で付与されます。自分で名前を付ける場合、重複が無いように気をつけて下さい。
またアクターはDIする事もできます。これについては後述します。
アクターとask
メソッド
{ case validForm => (try { libraryActor.ask(RequestBooks(validForm, ndlClient)).mapTo[BookData] } catch { case e: Exception => Future.successful(BookData(left(s"Server Error: $e"))) }).map { case BookData(\/-(books)) => Ok(Json.toJson(books)) case BookData(-\/(msg)) => InternalServerError(msg) } }
この部分で、Akkaアクターを利用しています。
今更ですが、アクターに関して説明します。アクターは「実行エンティティ」と呼ばれる存在で、それぞれ独立したコンテクストを持ち、処理を並行で実行する事ができます(並列で実行できるか否かは、アクターの実装に依ります。Akkaアクターの場合、並列に実行する事もあればしない事もありますが、これらはExecutionContextに依存します)。
独立して動くアクターに命令を出すには、「メッセージ」を送ります。メッセージはアクターが各々有している「メッセージボックス」(言語によってはメールボックスとも呼ばれます)に貯められ、アクターはボックスから1つずつメッセージを取り出し、メッセージに応じて処理を実行していきます。
アクターは基本的にメッセージ以外に依存しないので、アクターモデルでプログラムを書く事によって、自然とイベント駆動でリアクティブなロジックを組み立てる事ができます。
アクターにメッセージを送る方法としては、Erlang風の!
メソッドが有名ですが、ここではask
メソッドを使用しています。
!
メソッドは値を返しませんが、ask
メソッドは返り値を持つという違いがあります。返り値は、メッセージを受け取った側のアクターが、送信者に対して送り返すメッセージです。なので、受け取り側のアクター内に「送信者にメッセージを送り返す」という処理が実装されている必要があります。が、!
メソッドでメッセージを送って別メソッドで待ち受けるより簡便でわかりやすいので見通しの良いコードになります。
actor ! someMessage // これだと返り値はunitで、別に返事を待ち受ける必要がある。 actor.ask(someMessage) // これならFuture[Any]型の返り値が受け取れる。勿論Futureなので非同期実行。
なお、ask
メソッドには?
という別名があります。
actor ? someMessage
今回はRequestBooks
というcase classをメッセージとしてlibraryActor
に送っているわけですね。
RequestBooks
は、
case class RequestBooks(search: SearchForm, client: ActorRef)
と定義されています。
ちなみにアクターにメッセージを送る際は、timeoutを決めておかなければなりません。 今回は、外部APIを使う兼ね合いもあり、ちょっと長めに1分程取っています。
implicit val timeout: akka.util.Timeout = 1 minute
アクターとask
メソッドと返り値
上で
Future[Any]型の返り値が受け取れる
と書いたとおり、ask
メソッドの返り値はAny
型にアップキャストされてしまっています。
ですので、Future#mapTo
メソッド等で本来の型に戻してやらなければなりません。
この辺り型安全性が崩れてしまいますが、致し方ありません。
一応try
で囲んで例外が起きた時に捕捉するようにしてあります。
(エラーメッセージはあくまで開発用のものです)
libraryActor.ask((validForm, ndlClient)).mapTo[BookData]
BookData
の定義はこうです。
final case class BookData(books: String \/ Seq[Book])
scalazのEither型を利用しています。rightなら書誌情報のSeq
、leftならエラーメッセージが入っています。
この戻り値を、
{ case \/-(books) => Ok(Json.toJson(books)) case -\/(msg) => InternalServerError(msg) }
とパターンマッチして、rightの時は書誌情報一覧をJSON化して200で、leftの時はエラーメッセージを500で返すようにしています。
ちなみにこの時Future#map
を使っていますので、暗黙のExecutionContext
が必要です。
これはコードの先頭でインポートしてあります。
import play.api.libs.concurrent.Execution.Implicits.defaultContext
scalaで並行処理・並列処理を取り扱う場合、このExecutionContext
への理解が不可欠です。
Library
次に、メッセージを受け取るアクターの処理を見ていきます。
package actors import akka.actor._ import akka.pattern.AskTimeoutException import entities.Book import forms.SearchForm import play.api.libs.concurrent.Execution.Implicits.defaultContext import scala.concurrent.Future import scala.concurrent.duration._ import scala.languageFeature.postfixOps import scala.xml.XML import scalaz.\/ import scalaz.\/.{left, right} import shapeless._ import shapeless.ops.record.{Keys, Fields} import shapeless.syntax.std.traversable._ import shapeless.tag import shapeless.tag.@@ class Library extends Actor { import Library._ import akka.pattern.{ask, pipe} import NDLClient.QueryString override def receive = { case RequestBooks(SearchForm(None, None, None, _), _) => sender ! BookData(right(Seq.empty[Book])) case RequestBooks(search, ndlClient) => (try { ndlClient.ask(QueryString(queryUrlBuilder(search))).mapTo[NDLResponse] .map { case NDLResponse(res) => res.map { body => for { item <- XML.loadString(body) \\ "item" book <- bookFields.map { attr => (item \ attr).headOption.fold("")(_.text) }.toHList[genBook.Repr].map(genBook.from) } yield book } }.map(BookData) } catch { case e: AskTimeoutException => Future.successful(left(s"Request Failed: \n$e")) case _ => Future.successful(left("Something wrong...")) }) pipeTo sender } } object Library { def props = Props[Library] implicit val timeout: akka.util.Timeout = 1 minute final case class RequestBooks(search: SearchForm, client: ActorRef) final case class BookData(books: String \/ Seq[Book]) final case class NDLResponse(response: String \/ String) trait Cnt case class Search(title: Option[String], creator: Option[String], any: Option[String], cnt: Option[Int @@ Cnt]) implicit def toSearch(form: SearchForm) = { import form._ Search(title, author, any, count.map(tag[Cnt](_))) } lazy val genBook = Generic[Book] lazy val lgenBook = LabelledGeneric[Book] lazy val bookFields = Keys[lgenBook.Repr].apply.toList.map(_.name) lazy val lgenSearch = LabelledGeneric[Search] object toQueryString extends Poly1 { implicit def caseString[T <: Symbol] = at[(T, Option[String])] { case (k, Some(v)) => Some(k.name -> v) case _ => None } implicit def caseCnt[T <: Symbol] = at[(T, Option[Int @@ Cnt])] { case (k, ov) => Some(k.name -> ov.getOrElse(20).toString) } } private[Library] def queryUrlBuilder(search: Search) = Fields[lgenSearch.Repr].apply(lgenSearch to search).map(toQueryString).toList.flatten }
アクタークラス
アクタークラスはActor
をextends
して作ります。分かりやすくていいですね。
アクタークラスにはreceive
メソッドが無ければなりません。こいつはPartialFunction
を返すメソッドで、送られてくるメッセージに対して行う処理を記述します。
(他にもpreStart
やpostStop
のようなフックメソッドで挙動をコントロールする事もできます)
今回は2種類のパターンが記述してありますね。まず、
case RequestBooks(SearchForm(None, None, None, _), _) => sender ! BookData(right(Seq.empty[Book]))
ですが、これは要するに、「検索条件が空なら空の情報を返す」という事です。 検索条件が存在しないのですから、APIを叩いても当然何も返ってきません。
ですので、APIを叩く事なくさっさと結果を返してあげます。
メッセージの「送り主」はsender
メソッドで取得できます。
その下の、
case RequestBooks(search, ndlClient) => (try { ndlClient.ask(QueryString(queryUrlBuilder(search))).mapTo[NDLResponse] .map { case NDLResponse(res) => res.map { body => for { item <- XML.loadString(body) \\ "item" book <- bookFields.map { attr => (item \ attr).headOption.fold("")(_.text) }.toHList[genBook.Repr].map(genBook.from) } yield book } }.map(BookData) } catch { case e: AskTimeoutException => Future.successful(left(s"Request Failed: \n$e")) case _ => Future.successful(left("Something wrong...")) }) pipeTo sender
が、検索条件がある場合、つまり本来の場合の処理ですね。
ndlClient.ask(QueryString(queryUrlBuilder(search))).mapTo[NDLResponse]
.map { case NDLResponse(res) =>
再びask
メソッドが見えていますが、少し待って、まずはそのメソッドの引数を見てみましょう。
型消去
送っているのはQueryString
型のメッセージです。
final case class QueryString(queryString: Seq[(String, String)])
これは単純にSeq[(String, String)]
をラップするためのcase classですね。
何故こんな入れ物が必要なのかと言いますと、JVMでは型消去をしてしまうので、例えば
case stringSeq: Seq[(String, String)]
みたいに書いても、(String, String)
の情報は消えてしまっていて、取れないのですね。
なので、Seq(1,2,3)
とかでも受け取ってしまうのですが、当然これは実行時エラーを引き起こします。
その為、QueryString
クラスを定義しています。
またこういう状況ではなくても、1つのメッセージに対して1つのcase classを用意する事で、メッセージの意図を明確にする事ができますし、メッセージの形式を変更した場合でも、バグを起こす危険性が低くなるという助言を頂きました。
どういう事かと言いますと、メッセージ送信はどのような型でも行う事ができますが、ただし受信は、receive
メソッドで対応している型しか受け付けないので、例えば送信側のメッセージの型を変更し、受信側を変更し忘れた場合、コンパイルは通ってしまうのですが、送信側は永遠に返信の無いメッセージを送信し続ける事になる、という問題です。この手のバグは一般に発見し辛く、厄介です。しかしcase classでメッセージをラップすれば、メッセージの変更にはcase classの変更を伴う事になり、送信側と受信側での型の一致を保証する事ができます。
例を出します。以下の様なコードがあるとします。
// 送信側 val res = actor ? "Hello, world!" ... // 受信側 class HelloActor extends Actor { override receive = { case message: String => sender ! s"I receive '$message'" } }
文字列としてメッセージを受け取って、また文字列を返すだけのアクターです。
このアクターに、複数の文字列を受け取る事ができるように改良を加えたかったとします。
class HelloActor extends Actor { override receive = { case messages: Seq[String] => sender ! s"I receive '${ messages.mkString(",") }'" } }
しかし忙しく、受信側のコードを変えた段階で、送信する側の修正は後回しにしよう、と思ってしまいました。 後は、お察しの通り、そんな修正があった事などすっかり忘れてしまいました。
さてこのコードをコンパイルし、動かすとどうなるでしょうか。
// 送信側 val res = actor ? "Hello, world!" ... // 受信側 class HelloActor extends Actor { override receive = { case messages: Seq[String] => sender ! s"I receive '${ messages.mkString(",") }'" } }
このコードは正しくコンパイルされますし、また実行されます。
しかし思った通りには動きません。メッセージを受け取るHelloActor
クラスにString
型に対する処理が記述されていないからです。
このように単純な、送信側と受信側だけのコードでしたら、片方を修正し忘れる事など無いかもしれません。
ですが現実のプロダクトコードは膨大で、かつ複雑です。1つのアクターが送るべきメッセージは無数にありますし、受け取るメッセージも多いでしょう。何種類ものアクターにメッセージを送ったり、逆に受け取ったりする事もあります。
そんな状況で、うっかり一部のコードを修正し忘れるという事は、よくある事です。
そういう時、このメッセージをcase classに入れて送れば、
case class Message(value: String) // 送信側 val res = actor ? Message("Hello, world!") ... // 受信側 class HelloActor extends Actor { override receive = { case Message(message) => sender ! s"I receive '$message'" } } ...
メッセージの型を変えた時でも、
case class Message(value: Seq[String]) // 送信側 val res = actor ? Message("Hello, world!") // ここでコンパイルエラー! Message型のコンストラクタはStringを取らない! ... // 受信側 class HelloActor extends Actor { override receive = { case Message(messages) => sender ! s"I receive '${ messages.mkString(",") }'" } }
コンパイル時にエラーが起き、間違いを知る事ができます。
このようにして、バグを事前に、しかも分かりやすいエラーメッセージで知る事ができます。
さて、コードに戻りましょう。queryUrlBuilder
メソッドは、SearchForm
型の値を取り、List[(String, String)]
型の値を返すメソッドです。
SearchForm(Some("春琴抄"),None,None,None) SearchForm(None,Some("内田百閒"),None,None) SearchForm(None,None,Some("Scala"),Some(40))
という入力から、
List(("title", "春琴抄") List(("creator", "内田百閒")) List(("any", "Scala"), ("cnt", "40"))
という出力を作り出します。
では、このメッセージを送りつけるもう一つのアクターを見てみましょう。
NDLClient
package actors import actors.Library.NDLResponse import akka.actor.Actor import com.google.inject.{Inject, Singleton} import play.api.Configuration import play.api.libs.concurrent.Execution.Implicits.defaultContext import play.api.libs.ws.WSClient import scalaz.\/ import scalaz.\/.{left, right} @Singleton class NDLClient @Inject()(wsc: WSClient, config: Configuration) extends Actor { import NDLClient._ import akka.pattern.pipe def receive = { case QueryString(queryString) => wsc.url(ndlOpenSearchUrl(config)).withQueryString(queryString:_*).get().map { res => NDLResponse(res.status match { case 200 => right(res.body) case _ => left(s"Connect failed.\n${res.body}") }) } pipeTo sender } } object NDLClient { private[NDLClient] def ndlOpenSearchUrl(conf: Configuration) = conf.getString("settings.url.ndl.openSearch").get final case class QueryString(queryString: Seq[(String, String)]) }
アクターとDI
WSClient
やConfiguration
をDIで注入したかったので、DI管理下のクラスになっています。
アクタークラスをDI管理下に置くためには、アクターインスタンスを作成する為のモジュールを独自に作らなければなりません。
幸いAkkaにはAkkaGuiceSupport
が用意されているので、比較的簡単に書く事ができます。
Module
独自モジュールは次のようになっています。
package modules import actors.{NDLClient => NDLClientActor} import com.google.inject.AbstractModule import play.api.libs.concurrent.AkkaGuiceSupport class NDLClient extends AbstractModule with AkkaGuiceSupport { def configure = bindActor[NDLClientActor]("ndl-client-actor") }
たったこれだけです。これを、application.conf
で、
play.modules.enabled += "modules.NDLClient"
と指定してやるだけで、適切なインスタンスがDIされたアクターを名前で取得する事ができます。
名前というのは、この場合"ndl-client-actor"
の事です。
コントローラに書かれていた、
@Named("ndl-client-actor") ndlClient: ActorRef)
は、これを意味していたのですね。
WSライブラリ
処理に戻ります。
def receive = { case QueryString(queryString) => wsc.url(ndlOpenSearchUrl(config)).withQueryString(queryString:_*).get().map { res =>
WSClientは機能豊富ですが、ここではurl
メソッドでURLをセットし、withQueryString
メソッドでクエリストリングを付与した後、get
メソッドでリクエストを行っています。非同期に実行され、結果はFuture
で返されます。
NDLResponse(res.status match { case 200 => right(res.body) case _ => left(s"Connect failed.\n${res.body}") })
レスポンスは、ステータスを見て、200ならbodyをrightで、それ以外ならエラーメッセージをleftで返しています。 (繰り返しになりますが、ここでのエラーメッセージはあくまで開発用のものです)
pipeTo
この結果を呼び出し元に返します。
} pipeTo sender
ここで、!
ではなくpipeTo
を使っています。
pipeTo
は、その名の通りアクターからアクターへ処理をパイプするときに使うメソッドです。
この場合、返り値の型はFuture[NDLResponse]
になるのですが、もしこれを!
メソッドで返してしまうと、ask
メソッドの返り値がFuture
でラップされる事から、二重にFuture
でラップされてしまう事になります。
これを防ぐ為、flatMap
を使うようなイメージで、pipeTo
で値を送り返してやります。
再びLibrary
さて、Libraryでの処理に戻ってきました。
取得したレスポンスを処理していきましょう。
.mapTo[NDLResponse]
.map { case NDLResponse(res) =>
res.map { body =>
まず返ってきた値からレスポンスボディを取り出します。
scalazのEither(\/
)を使っているので、もし正しくないレスポンスだった場合、この以下の処理はスキップされ、leftがそのまま返されます。
for { item <- XML.loadString(body) \\ "item" book <- bookFields.map { attr => (item \ attr).headOption.fold("")(_.text) }.toHList[genBook.Repr].map(genBook.from) } yield book
そして bodyの文字列をXMLとして解析し、必要な情報を濾し取ってBook
型に変換し、Seq
として返します。
後はこの結果を、
}.map(BookData) } catch { case e: AskTimeoutException => Future.successful(left(s"Request Failed: \n$e")) case _ => Future.successful(left("Something wrong...")) }) pipeTo sender
メッセージの送り主、即ちコントローラにpipeTo
で送り返してあげれば、コントローラは最終的に書誌情報が手に入るという事です。
ちょっと内部実装の話
さて、コードをベースに今まで語ってきましたが、最後に、Scalaの並行実行について少しだけ解説します。
Scalaの並行処理は、単に新しいスレッドを立ててそちらに処理を任せる、という単純なものではありません。
ではどのように動くかといいますと、それはExecutionContext
によります。例えばサンプルコード等によく書いてあるscala.concurrent.ExecutionContext.Implicits.global
だと、CPUの個数分しか並行実行しません。並列で実行できる最大限で並行処理をしようとしているわけですね。
ではそんなglobal
を使って、例えば8個の並行処理を行おうとした場合どうなるか。こういう時は、4個のスレッドにそれぞれ2処理、いい感じに割り当てられます。さらに処理が増えた場合は、暇してるスレッドに優先的に割り振られていきます。
thread1 | 処理1 | 処理5 | 処理8 | thread2 | 処理2 | 処理6 | 処理10(実行中) thread3 | 処理3 | 処理7 | 処理9(実行中) thread4 | 処理4(実行中) 新規処理: 処理11 <- こいつはthread1に割り振られる
これが何を意味しているかと言いますと、空いているスレッドが無ければ処理は実行されないという事です。
4コアCPUの環境で標準のglobal
を使い、まず重い処理を4つ走らせたとしましょう。その後、軽くて優先度が高い処理がどれだけ発生しても、まず最初の4つの処理が終わらなければ後続の処理は何時までたっても始まりません。
thread1 | 処理1(実行中) thread2 | 処理2(実行中) thread3 | 処理3(実行中) thread4 | 処理4(実行中) 新規処理: 処理5, 処理6, 処理7, 処理8, ...
なので、ブロッキングするような処理をどんどん並行で実行していると、並行処理しているはずなのに速度が出ない、という事態に陥ります。単に「重い処理」ではなく「ブロッキングを伴う処理」というのが肝です。重い処理はどうしようもない点がありますが、ブロッキングであれば別スレッドに分けるだけで回避ができます。
ですので、そういう処理には独自のExecutionContext
を割り当てる等、対策を講じた方が良いでしょう。
ここで述べたような内容は、Scalaの並行処理(Future
とか)を扱った文脈では必ずと言っていい程ちゃんと解説がなされています。御存じの方も多かったでしょう。そんな事をここでも繰り返し述べたのは、それだけ重要だからです。
処理が詰まる可能性がある、という危険性は、Scalaで並行処理を行う上で必ず頭のどこかに留めておくべきです。
で、
それほど強く言ったのに、今回のサンプルコードではあまりそういう危険性に対する配慮を行っていませんね。ExecutionContext
もplay.api.libs.concurrent.Execution.Implicits.defaultContext
を何も考えずに使っています。
これは面倒くさかったからあくまでサンプルなので簡潔さを優先したからです。
実用的なコードを書く際は、是非ともご注意下さい。
結び
Play FrameworkとAkkaを利用して作られた簡単なWebアプリケーションの、Akkaに関する解説を行いました。 アクターモデルで構築されたアプリケーションには以下の様な利点があります。
- 並行性・並列性
- 耐障害性
- 疎結合性
これらの利点は、いずれも大規模アプリケーションを作成する際にこそ強い影響を持つものですので、今回の例だと少し有り難みが薄かったかもしれません。 また、耐障害性に関しては、今回の例で分かりやすいサンプルを提示する事ができませんでした。
しかし、アクターモデルの「メッセージを介してアクター同士が処理を連鎖する」という動きは摑んでいただけたかと思います。
以上で簡単ですが、Akkaに関する解説とさせて頂きます。
アクターモデルの本領はもっとアクター数が増加した時に発揮されますので、是非ともAkkaを使ったアプリケーションを試してみてください。 また、業務でこれらの技術を使いたい場合はぜひ弊社にご連絡頂ければと思います。
更新履歴
- 2016年4月22日 注意事項を追記