LoginSignup
0
1

More than 3 years have passed since last update.

非同期関数を同期関数のように扱えるawait機構をさわってみた

Posted at

イントロ

非同期化された関数を同期的に使用できた方がコードの見通しがよくなることが結構ありますが、そのように「非同期関数を動機的に処理」するための await機構についての整理。。ついでに関数を非同期化する機構であるPromiseasyncなどについて。

実行環境はなぜか Cloud Functions for Firebase のローカル環境ですが、特にFunctionsである必要性はありません。
たまたまココで作業してて、そのままその環境で稼働確認してるだけ、、です。

環境の準備は、Cloud Functions for Firebase をFirebase Hostingへデプロイするための環境構築手順 このあたりのFunctionsの構築手順をやればよろしいかと思います。

やってみる

まずは初期状態

まず、時間がかかるexecute関数があるとします。

utils.ts
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を作成します。

index.ts
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を使うとこうなります。

utils.ts
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を返す関数は、下記のように使用します。

index.ts
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は不要なので削除)

utils.ts
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を以下のように非同期関数としてあつかうことができるようになります。

index.ts
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機構ですが、awaitPromiseインスタンスを返すように記述された関数だったり asyncで修飾された関数だったり、とにかく非同期の関数について、終了するまでそこで待ってくれるという機構なんだそうです。さきのasyncされたexecute関数を下記のように同期関数のように呼び出す事が出来ます。

index.ts
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をつけること」awaitasyncな関数内でしか使用できないこと」 がルールです。

一応ですが実行結果は、、、

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で全体をくるむ必要がある

以上、おつかれさまでした。

関連リンク


  1. Futureパタンは別スレッドでデータを取得させておいて、ホントに必要になったときに Future#getResult()をよびだして実データをもらう(まだならさすがにそこでwaitさせる) ってので、結果を取得する方式が微妙に異なりますが、、。 

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1