イントロ
非同期化された関数を同期的に使用できた方がコードの見通しがよくなることが結構ありますが、そのように**「非同期関数を動機的に処理」**するための await
機構についての整理。。ついでに関数を非同期化する機構であるPromise
やasync
などについて。
実行環境はなぜか Cloud Functions for Firebase のローカル環境ですが、特にFunctionsである必要性はありません。
たまたまココで作業してて、そのままその環境で稼働確認してるだけ、、です。
環境の準備は、Cloud Functions for Firebase をFirebase Hostingへデプロイするための環境構築手順 このあたりのFunctionsの構築手順をやればよろしいかと思います。
やってみる
まずは初期状態
まず、時間がかかるexecute
関数があるとします。
const me = {
/**
* もとの、時間がかかる関数。
* @param num
*/
execute (num) {
// 時間がかかる(5秒)処理
// console.log('execute start.')
this.sleep(5000)
// console.log('execute 5秒経過')
if (num == 100) {
// console.log('execute エラー終了')
throw new Error('100だけダメっていう関数なんです')
}
// console.log('execute 正常終了')
return num * 10
},
// https://qiita.com/albno273/items/c2d48fdcbf3a9a3434db 感謝!(おもいっぽいので実用上はご注意)
sleep (time) {
const d1: any = new Date()
while (true) {
const d2: any = new Date()
if (d2 - d1 > time) {
return
}
}
}
}
export default me
if (!module.parent) {
console.log(me.execute(10))
}
Functionsを作成します。
import * as functions from 'firebase-functions'
import utils from './utils'
export const helloWorld = functions.https.onRequest((request, response) => {
const num = request.query.num
try {
const result = utils.execute(num)
console.log(result)
response.send(`${result}`)
} catch (error) {
console.log(error.message)
response.send(error.message)
}
console.log('End.')
})
ビルドしてローカルでFunctionsを起動します。
$ cd functions/ && npm run build && cd ../
$ firebase serve --only functions
別のターミナルからcurlで関数を呼び出してみます。たとえば下記の通り。
$ curl http://localhost:5000/fb-samples-xxxx/us-central1/helloWorld -d 'num=50' -G
500
$ curl http://localhost:5000/fb-samples-xxxx/us-central1/helloWorld -d 'num=100' -G
100だけダメっていう関数なんです
$
5秒たってたから、処理が返ってきましたね。コンソールを見てみると
info: User function triggered, starting execution
info: execute start.
info: execute 5秒経過
info: execute 正常終了
info: 500
info: End.
info: Execution took 5019 ms, user function completed successfully
info: User function triggered, starting execution
info: execute start.
info: execute 5秒経過
info: execute エラー終了
info: 100だけダメっていう関数なんです
info: End.
Execution took 5005 ms, user function completed successfully
うん、上から順番に同期的に順次実行されていますね。
Promiseで非同期化する
さて、遅いexecute
関数をPromise機構を使って非同期化します。Promise
はデザインパタンでいうFutureパターンのように、とりあえずなにかしらのチケット(Promise)を返却し、処理が完了したらそのチケットにあらかじめ登録しておいた関数をコールバックするような方式です。1
そのコールバックされる関数の引数に、実行結果が載せられてくるようになっています。
さてPromise
を使うとこうなります。
const me = {
/**
* オリジナルの関数をPromiseで非同期化した
* @param num
*/
asyncExecute (num) {
return new Promise((resolve, reject) => {
try {
const result = this.execute(num)
resolve(result)
} catch (error) {
reject(error)
}
})
},
... 以降はおなじなので割愛
もとの関数が正常終了したら resolve
関数を呼び出して、実行結果をセットしています。
もとの関数が異常終了したら、reject
関数を呼び出して、errorインスタンスをセットしています。
非同期化した関数を作成しました。このPromise
を返す関数は、下記のように使用します。
import * as functions from 'firebase-functions'
import utils from './utils'
export const helloWorld = functions.https.onRequest((request, response) => {
const num = request.query.num
const resultPromise = utils.asyncExecute(num)
resultPromise
.then(result => {
console.log(result)
response.send(`${result}`)
})
.catch(error => {
console.log(error.message)
response.send(error.message)
})
console.log('End.')
})
.then
,.catch
にコールバックしてほしい関数をセットしています。
ビルドしてローカルで起動して、curlしてみたときのコンソールに出力される内容は以下の通り
info: User function triggered, starting execution
info: execute start.
info: execute 5秒経過
info: execute 正常終了
End.
info: 500
info: Execution took 5019 ms, user function completed successfully
info: User function triggered, starting execution
info: execute start.
info: execute 5秒経過
info: execute エラー終了
info: End.
info: 100だけダメっていう関数なんです
info: Execution took 5023 ms, user function completed successfully
うん、実行結果より先にEndが返ってきています。ホントは5秒待つことなくEndが出力されると思ったのですが、、どうやら待っちゃってますね、、なんでかな、、。
asyncで非同期化する
さて先ほどは、非同期化するためのPromise
を返す関数でラップしましたが、async
機構を使うと、そのラッピングを自動化してくれます。具体的には execute
関数にasync
をつけるだけです。(ついでにasyncExecute
は不要なので削除)
const me = {
/**
* もとの時間がかかる関数。
* @param num
*/
async execute (num) {
// 時間がかかる(2秒)処理
console.log('execute start.')
this.sleep(5000)
console.log('execute 5秒経過')
if (num == 100) {
console.log('execute エラー終了')
throw new Error('100だけダメっていう関数なんです')
}
console.log('execute 正常終了')
return num * 10
},
// https://qiita.com/albno273/items/c2d48fdcbf3a9a3434db
sleep (time) {
const d1: any = new Date()
while (true) {
const d2: any = new Date()
if (d2 - d1 > time) {
return
}
}
}
}
export default me
if (!module.parent) {
console.log(me.execute(10))
}
async
をつけると、その関数がPromise
インスタンスを返すようになってくれるそうで、従って呼び出し側は、同期関数だったexecute
を以下のように非同期関数としてあつかうことができるようになります。
import * as functions from 'firebase-functions'
import utils from './utils'
export const helloWorld = functions.https.onRequest((request, response) => {
const num = request.query.num
const resultPromise = utils.execute(num)
resultPromise
.then(result => {
console.log(result)
response.send(`${result}`)
})
.catch(error => {
console.log(error.message)
response.send(error.message)
})
console.log('End.')
})
なるほど。正常終了も異常終了も、さきほどのasyncExecute
と全く同じようにあつかうことが出来るみたいです。。async
便利だ、、、。
呼びだし元のコードをawaitで同期っぽく書く
さてさて、ようやくawait
機構ですが、await
はPromise
インスタンスを返すように記述された関数だったり async
で修飾された関数だったり、とにかく非同期の関数について、終了するまでそこで待ってくれるという機構なんだそうです。さきのasync
されたexecute
関数を下記のように同期関数のように呼び出す事が出来ます。
import * as functions from 'firebase-functions'
import utils from './utils'
export const helloWorld = functions.https.onRequest( async (request, response) => {
const num = request.query.num
try {
const result = await utils.execute(num)
console.log(result)
response.send(`${result}`)
} catch (error) {
console.log(error.message)
response.send(error.message)
}
console.log('End.')
}
)
いちばん最初の同期関数execute
を普通に呼び出したときとほとんどおなじ書き方で、非同期関数を呼ぶことが出来ることがわかると思います。
「非同期関数に await
をつけること」 と 「await
はasync
な関数内でしか使用できないこと」 がルールです。
一応ですが実行結果は、、、
info: User function triggered, starting execution
info: execute start.
info: execute 5秒経過
info: execute 正常終了
info: 500
info: End.
Execution took 5020 ms, user function completed successfully
info: User function triggered, starting execution
info: execute start.
info: execute 5秒経過
info: execute エラー終了
info: 100だけダメっていう関数なんです
info: End.
Execution took 5009 ms, user function completed successfully
Endの表示がちゃんと非同期関数の戻りを待ってることがわかります。
ほぼ一番最初のコードに戻りました。自分で非同期化した関数を同期ぽく呼び出す、ってなにやってんのって感じですが、仕組みは理解できました。
まとめると
- ある関数を
Promise
を返す関数でラップすることで、非同期的に使用することが可能になる -
async
機構を使うことで、同期関数 → 非同期関数 に変更する事が出来る(Promise化できる) -
await
機構を使うことで、非同期関数 → 同期関数 のように取り扱うことが出来る - その際は
async
で全体をくるむ必要がある
以上、おつかれさまでした。
関連リンク
-
Futureパタンは別スレッドでデータを取得させておいて、ホントに必要になったときに
Future#getResult()
をよびだして実データをもらう(まだならさすがにそこでwaitさせる) ってので、結果を取得する方式が微妙に異なりますが、、。 ↩