読者です 読者をやめる 読者になる 読者になる

ScalaでC#のasync, awaitを実現するライブラリ「async」の紹介

この記事は Iwate Advent Calendar 2014 の22日目の記事です。昨日はnana4gontaさんのFirefox Developer Editionを使って他ブラウザをリモートデバッグする - Qiitaでした。明日はayokuraさんです。

岩手関連の記事を書こうと思い、仕事で使おうとしてるD3.jsを使って岩手県を書こうとしたらズバリそのままのサイトを発見して挫折しました。

岩手県ぬりえ
http://acuerdo.m18u.net/iwate_nurie/

結局思いつかなかったので全然関係ない話題を書きます。

さて、Scalaで非同期処理を扱うにはFuturePromiseという仕組みを利用します。(ここでは、標準ライブラリのものを指してます。) FuturePromiseは非同期で処理する、まだ存在しない処理結果を扱うための仕組みで、複数の非同期処理結果を扱うためには基本的にコールバック(mapflatMapfor式など)を使用するため、コーディング量が若干多くなります。

一方C#では、非同期処理を扱うための構文としてasync修飾子とawait演算子が用意されています。これらを利用することで、非同期処理を同期処理のように書くことができ、コーディングの内容が比較的見やすくなっています。

今回は、C#asyncawaitScalaで実現したライブラリ「async」を紹介したいと思います。

2つの言語の非同期処理を比較する

今回の記事では以下の非同期処理を書き、実際にどのようなコードになるかを比較してみます。

  • 数値を非同期で取得するgetInt関数
  • 非同期で取得した2つの数値を足すadd関数

Scalaの実装

まずはScalaの実装です。

import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global

def getInt(x: Int): Future[Int] = Future {
  Thread.sleep(1000)
  x
}

def add(x: Future[Int], y: Future[Int]): Future[Int] = {
  for {
    a <- x
    b <- y
  } yield a + b
}

println( Await.result(add(getInt(1), getInt(2)), 10.seconds) )

add関数で、非同期で処理される2つの数値を足すという非同期処理をしています。この程度の処理であればすんなり理解できますが、もっと多いFutureを扱う場合などは、処理結果をブロッキングしないように扱うために気を使ってコードを書く必要があります。(単に私のScalaレベルが低いだけですが・・・)

C#の実装

次に、C#での実装です。 (ここでは、C#の慣習のならってメソッド名をGetIntAsyncAddAsyncと変えて実装しています。)

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
  public static void Main()
  {
    var program = new Program();

    var x = program.GetIntAsync(1);
    var y = program.GetIntAsync(2);

    Console.WriteLine( program.AddAsync(x, y).Result );
  }

  public Task<int> GetIntAsync(int x)
  {
    return Task.Run<int>(() => {
      Thread.Sleep(2000);
      return x;
    });
  }

  public async Task<int> AddAsync(Task<int> x, Task<int> y)
  {
    return await x + await y;
  }
}

注目していただきたいところはAddAsyncメソッドです。非同期で処理される値を足す処理ですが、非同期処理の結果に対してasync演算子を追加することで、同期処理と同じように処理を書くことができます。Scalaadd関数と見比べるとこちらのほうが見やすいのではないかと思います。

async演算子は、式を書ける場所にはどこにでも追加できるため、扱う非同期処理が多くなっても基本的に同期処理と同じように書くことが可能です。

Scalaでasync, awaitを実現するライブラリ「async」について

上で紹介したC#asyncawaitについては、SIP 22Scalaの標準ライブラリに含むかどうかが検討されており、現在は別ライブラリ「async」として提供されています。

scala/async
https://github.com/scala/async

このライブラリを利用することで、Scalaでも同期処理のようにコーディングすることができます。上で書いたScalaの実装を、このライブラリを利用して書きなおしたものが以下のコードです。(上で書いたScalaの実装と区別するために、関数名にAsyncをつけました。)

import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global

import scala.async.Async.{async, await}

def getIntAsync(x: Int): Future[Int] = async {
  Thread.sleep(1000)
  x
}

def addAsync(x: Future[Int], y: Future[Int]): Future[Int] = async {
  await(x) + await(y)
}

println( Await.result(add(getInt(1), getInt(2)), 10.seconds) )

いかがでしょうか。await関数を利用することで、Futureの値をあたかも同期処理のように記述することができ、見やすいコードになったと思います。

awaitはただの関数ですので、C#同様に式が書けるところにはどこでも書くことができます。(C#awaitasyncをつけたメソッド内にしか書けないことと同様、Scalaawaitasync関数の中にしか書けません。) このため、多くのFutureを扱う場面においても、見やすいコードが書けるのではないかと思っています。早く標準ライブラリに入って欲しい・・・!

使い方

依存ライブラリに追加して、関数をimportするだけで利用できます。

(sbt)

libraryDependencies += "org.scala-lang.modules" %% "scala-async" % "0.9.2"

(import)

import scala.async.Async.{async, await}

なお、実装はマクロとコンパイラプラグインで頑張っている模様です。
(追記)実装はマクロのみでした。失礼しました。コメントくださったxuwei_kさん、ありがとうございます!

制限

関数の引数が名前渡しの場合、awaitの戻り値をそのまま渡せないようです。

def increment(x: => Int): Int = x + 1

async {
  increment( await(getIntAsync(0)) )
}

この場合は、awaitの結果を一旦変数に入れてから利用します。

async {
  val x = await(getIntAsync(0))
  increment(x)
}

その他の制限については、GitHubのプロジェクトやIssuesを確認してみてください。

まとめ

C#asyncawaitScalaで利用できるライブラリを紹介しました。SIP 22で提案はされていますがPendingとなっており、SIPを詳しく追いかけているわけではないため、現在どのようなステータスとなっているかまでは分かりません。

仕事ではScalaよりもC#を使っているため、多少のバイアスはかかっていますが、今回の記事で紹介したとおり、コードが見やすくシンプルになると思います。ぜひ標準ライブラリ入りして欲しいですね!

気になる方はぜひ使ってみてください。