Introduction
突然ですが、皆さん async/await は知っていますか!?
async/await といえば ES7 での仕様策定に向けて議論が進められている ECMAScript の新機能で、非同期処理を同期処理っぽく書く事ができます(ルーツは C# のasync/await らしいです)。
async/await について理解を深める為に、実際に(現在提案されてる仕様での)使い方を見てみましょう。
以下は http://wiki.ecmascript.org/doku.php?id=strawman:async_functions からの抜粋です。
1 var request = require('./request.js');
2 var headers = { 'User-Agent': 'lukehoban', 'Authorization': 'token 665021d813ad67942206d94c47d7947716d27f66' };
3
4 // Promise-returning asynchronous function
5 async function getCollaboratorImages(full_name) {
6 // any exceptions thrown here will propogate into try/catch in callers - same as synchronous
7 var url = 'https://api.github.com/repos/' + full_name + '/collaborators';
8 // await a promise-returning async HTTP GET - same as synchronous
9 var [response, body] = await request({url: url, headers: headers});
10 return JSON.parse(body).map(function(collab) {
11 return collab.avatar_url;
12 });
13 }
ちょっと複雑すぎる例になってますが、注目すべきは 9行目 の var [response, body] = await request({url: url, headers: headers});
です。この行では、request({url: url, headers: headers})
で http リクエストを投げて、その結果を response, body
に代入しています。さらに続く10行目以降で、 body
が関数の返り値の生成に使われています。
このコードだけを見ていると、9行目では同期的に http request が投げられ、その response が返ってくるのを待って(Network IO でブロックして)、 resopnse, body
の代入が行われている様に見えます。しかし、実際にはそうではありません。実は、上記のコードは以下の様なコードと等価になります。
// Promise版
1 var request = require('./request.js');
2 var headers = { 'User-Agent': 'lukehoban', 'Authorization': 'token 665021d813ad67942206d94c47d7947716d27f66' };
3
4 function getCollaboratorImages(full_name) {
5 var url = 'https://api.github.com/repos/' + full_name + '/collaborators';
6 return request({url: url, headers: headers}).then(function(value) {
7 var [response, body] = value;
8 JSON.parse(body).map(function(collab) {
9 return collab.avatar_url;
10 });
11 })
12 }
Promise が使われていて、見慣れたコードになっていますね。ポイントは getCollaboratorImages
の返り値が Promise
オブジェクトになっている事、また request({url: url, headers: headers})
が返す Promise
オブジェクトに対しては then
で続きの処理を書いている事です。then
によって callback の処理を登録している為、完全に非同期に実行されるコードとなっています。
Promise を使ったコードと async/await を使ったコードが、なぜ等価となるのでしょうか?
鍵になるのは、ES6で導入された Generator
という機能です。
ES6 でも Generator と Promise を使うと async/await が実装できる
実は、Generator と Promise を組み合わせる事で async/await (と同等の機能を果たす関数)を実装する事が出来ます。
これに関しては、 ES6 Generatorを使ってasync/awaitを実装するメモ が詳しいので興味が湧いた人は読んでみてください。
上記記事で注目すべきなのは、Generator をコルーチンとして使っていて、Promiseと組み合わせていることです。この2つが async/await を実装する為に必要なパーツとなります。
コルーチンは、雑に説明すると「処理の中断や再開が可能な手続き処理」を指します。有名どころだと、Lua や C# が言語レベルでコルーチンをサポートしています。コルーチンの呼び出し側とコルーチン側で制御を行き来する事が出来て、その際に値の受け渡しもできるのが特徴です。
逆に言えば、コルーチンと Promise があれば async/await は実装できるわけです。さて、ここで本題にいきたいと思いますが、 Ruby で コルーチンといえば Fiber
ですよね?
という事で、Fiber
を使って、簡単に async/await を実装してみました。
Ruby で Fiber を使って async/await を実装する
それでは、Ruby で async/await を実装してみたいと思います。
今回はサンプル実装なので、例外の発生は考慮せず、Promise
も以下の様な initialiize
と then
だけが定義された簡単なものを考えます。
1 class Promise
2 def initialize(callback)
3 @callback = callback
4 end
5
6 def then(resolve = ->() {}, reject = ->() {})
7 @callback.call(resolve, reject)
8 end
9 end
これは、例えば以下の様に使います。
1 require 'eventmachine'
2
3 def sleep(sec)
4 Promise.new(->(resolve, reject) {
5 EM.add_timer(sec) do
6 resolve.call('you sleep ' + sec.to_s)
7 end
8 })
9 end
10
11 EM.run do
12 sleep(0.3).then(->(value) {
13 p value
14 })
15 p 'start'
16 end
17 #=> "start"
18 #=> "you sleep 0.3"
sleep
メソッドの 返り値である Promise
オブジェクトに対して、then
で続きの処理(callback)を渡しています。sleep
の動作が「渡された数字の秒数だけ待ってから、then
で登録された callback を実行する」というものになっている為、上記のコードを事項すると15行目の p 'start'
で "start"
が出力されてから、13行目の p value
で "you sleep 0.3"
が出力されます(尚、即時 return してから登録した callback を実行するという動作を実現する為に EventMachine
を使っています。EM.run
に渡したブロックの中では様々な non-blocking api が使えて、例えば上記の例では JS の setTimeout
的な振る舞いをする EM.add_timer
を使っています)。
さらに Promise
を返す別のメソッドとして以下の様な decorate_message
を定義して、sleep に続けて使う func
を考えます(僕が雑に定義した Promise
の then
メソッドは Promise
オブジェクトを返さない為、chaining はできてませんが気にしないでください)
1 def decorate_message(message)
2 Promise.new(->(resolve, reject) {
3 http = EM::HttpRequest.new('http://google.com/').get
4 http.callback {
5 resolve.call(message + ': ' + http.response[0..100])
6 }
7 })
8 end
# use Promise
1 def func(time)
2 sleep(time).then(->(message) {
3 p message
4 decorate_message(message).then(->(decorated_message) {
5 p decorated_message
6 })
7 })
8 end
9
10 EM.run do
11 func(0.3)
12 p 'start'
13 end
14 #=> "start"
15 #=> "you sleep 0.3"
16 #=> "you sleep 0.3: <HTML><HEAD><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">\n<TITLE>302 Moved</TITL"
この func
を今から定義する async
, awai
メソッドをつかって書き換えると、以下の様になります。sleep
や `decorateなんだかそれっぽく見えますね!!
# use async/await
1 async :asyncFunc do |time|
2 message = await sleep(time)
3 p message
4 decorate_message = await decorate_message(message)
5 p decorate_message
6 end
7
8 EM.run do
9 asyncFunc(0.3)
10 p 'start'
11 end
12 #=> "start"
13 #=> "you sleep 0.3"
14 #=> "you sleep 0.3: <HTML><HEAD><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">\n<TITLE>302 Moved</TITL"
次に、ここで使った async
メソッドと await
メソッドの実装を見てみたいと思います。
Fiber を使った async/await の実装
一番シンプルな例として、 Promise
の then
で resolve
だけが指定されるケースを考える(かつ例外の発生も考慮しない)と、以下のコードで async/await が実現できます。たった19行の短いコードです。
# implementation of async/await
1 def async_internal(fiber)
2 chain = ->(result) {
3 return if result.class != Promise
4 result.then(->(val) {
5 chain.call(fiber.resume(val))
6 })
7 }
8 chain.call(fiber.resume)
9 end
10
11 def async(method_name, &block)
12 define_method method_name, ->(*args) {
13 async_internal(Fiber.new { block.call(*args) })
14 }
15 end
16
17 def await(promise)
18 Fiber.yield promise
19 end
注目するポイントは 16行目 の Fiber.new
と、 5行目 の fiber.resume
、 21行目 の Fiber.yield
です。Fiber
はコルーチンの機能を提供するためのモジュールで、 Fiber.new
にコルーチンにしたい処理をブロックで渡して使います。
Fiber
の挙動は、以下のコードを見るとイメージしやすいかもしれません。Fiber.new
に渡した処理は、生成した fiber
に対して resume
を呼び出す事で実行されます。以下の例では、 13行目 で fiber.resume(i)
を実行しており、このタイミングで "init fiber"
が出力されます。ただし、5行目 で Fiber.yield
が呼ばれると、処理がストップして制御は 13行目 に戻ります。この様に、コルーチンの「処理を途中で抜ける」機能は Fiber.yield
によって実現されます。
# fiber example
1 fiber = Fiber.new do
2 p 'init fiber'
3 n = 0
4 loop do
5 result = Fiber.yield(n)
6 p "after yiled: #{result}"
7 n += 1
8 end
9 end
10
11 i = 10
12 4.times do
13 result = fiber.resume(i)
14 p "after resume: #{result}"
15 i += 1
16 end
17 # >> "init fiber"
18 # >> "after resume: 0"
19 # >> "after yiled: 11"
20 # >> "after resume: 1"
21 # >> "after yiled: 12"
22 # >> "after resume: 2"
23 # >> "after yiled: 13"
24 # >> "after resume: 3"
Fiber.yield
には引数を渡す事が出来て、渡した引数は fiber.resume
の返り値となります。逆に、fiber.resume
に引数を渡す事も出来て、この場合は Fiber.yield
から処理を再開する際にその返り値として使う事ができます。返り値の関係は少しややこしいですが、あくまで「Fiber.yield
が通常の return
に相当するもの(よって fiber.resume
の返り値になる)」、「fiber.resume
の2回目以降の呼び出しは処理の再開を意味していて、再開時に初期値を渡す事ができる」みたいに考えておくと良いと思います。(よく見ると fiber.resume
の最初の引数は使われていない事がわかると思います)
Fiber
を使う事で、 Fiber.yield
と fiber.resume
で制御を行ったり来たりしながら協調して作業を進める事ができます。
ここで async
, await
の実装に戻ると、await
はただ Fiber.yield
を実行しているだけ、また async
も実行したい処理をブロックとして受け取って Fiber
オブジェクトを作り出し、async_internal
に渡しているだけである事がわかります。
async_internal
がメインのロジックですが、ここもそれほど複雑な事はしていません。chain
という名前で再帰的に実行される Proc
オブジェクトを作り、メソッドの最後に chain.call
を呼ぶことで再帰処理を開始しています。
chain.call
の引数に fiber.resume
を書いているのがポイントで、fiber.resume
によってコルーチンの処理が開始され、Fiber.yield
(つまり await
)が呼ばれるまで処理が続きます。await
に出くわすとfiber.resume
を呼び出した先に制御が返りますが、その際に返り値は Promise
となっていて、それが chain
の引数になります。chain
の中では引数に対して then
で続きの処理を書いて、再び fiber.resume
を呼び出します。この時、fiber.resume
に初期値として渡した値がコルーチンの処理の再開時に await
の返り値として取得できます。
以上の様なフローによって、async
と await
を使う事で同期的処理の様に書きながらも非同期処理を実現する事ができました。1
まとめ
- ES7 での仕様策定が進められている async/await を使うと、非同期処理を同期処理っぽい syntax で記述できる
- async/await は、Generator をコルーチンとして利用し、さらに Promise と組み合わせる事で実現できる
- コルーチンは、 JavaScript 以外にも Ruby や Lua など様々な言語で提供されている機能である為、様々な言語で async/await は実現できる(と思う)
参考資料
-
Ruby 版
async
はPromise
を返さないとか、Promise
がそもそも chaining できないとか細かな差異はいろいろありますが、その辺は API をどうするかという問題なので、適宜書き換えれば調整はできると思ってます。 ↩