非同期プログラミングは難しいね。やっぱり。 - jekyllのような静的ブログジェネレータをnode.jsで作る(3)

公開:2017-07-03 19:00
更新:2020-02-15 04:37
カテゴリ:静的ブログジェネレータ,非同期

静的ブログジェネレータの製作は進めている。 進捗は以下のとおりである。

といったところである。

非同期プログラミングというのは難しい。私はPromiseを使っているので、Callback Hellには陥っていないものの、Promise Hellに陥りつつあり、これを回避すべく最近nodeで使えるようになったasync/awaitを使うことにした。
しかしこのasync/awaitというのも結構易しく見えて難しいもののひとつである。同期的な処理のように書けて非常にコードがわかりやすい。 が、実のところわかりやすく見えるだけで、非同期処理には変わりない。結局リソースコントロールの難しさやメソッドの起動タイミングの不明瞭さに直面してにっちもさっちもいかなくなってしまうのである。async/awaitで書くと非同期処理で書いていることを忘れてしまい、そのことが混乱に拍車をかけてしまったりする。そのうちasync/await hellという言葉も出来たりして。。

書いてみて難しいと感じたのは、再帰的処理と非同期メソッドの組み合わせである。この処理は実際の挙動を事前に把握してコードを書くことは難しいと感じた。書いてみて動きを確かめ、そしてコードを修正するという進め方が一番良いようだ。非同期処理のイディオムがまだ身についていないのも多分にあるだろうけれども、同期処理に比べて同じことをするにも考えなければならないことが飛躍的に増えるのは間違いなく、そのような挙動は実際に確かめるのが一番良いと思う。

しかしなぜこのようなプログラミングが難しい非同期処理をnodeは取り入れたのだろうか。それはJSがシングルスレッドで動作するからである。同期的メソッドだけだと、リソースアクセスで待ちが発生して、ブロックされてしまい、それが終わるまで次の処理ができないからである。これは私のように一人で使用するようなアプリやツールを作るのであればあまり問題にはならないが、Webサービスとかだと複数の要求を遅滞なく裁かないといけないので、1つの要求上でブロックしてしまうと、それ以降の要求をずっと待たしてしまうことになってしまうのである。それを回避するためにはリソースアクセスでブロックが発生した場合、ブロックが解除されて続きの処理ができるようになるまではほかの要求の処理ができるようにする必要がある。これを可能にするのが非同期コールバックという考え方である。nodeでいうところの非同期コールバックは割り込みという考え方に近い。これを実装するためにnodeではlibuvというライブラリを使用している。シングルスレッドで非同期コールバックを実現するためにマルチスレッドを使用しているというのが興味ぶかいところでもある。

この非同期処理のプログラミングの難しさを考えると、マルチスレッドのほうが楽かというとそうではない。マルチスレッドであればリソースブロックの問題は比較的容易に解決できるが、その代わりリソース競合という問題が起きる。このリソース競合を調整し、スレッドセーフにするために同期オブジェクト(クリティカルセクション・ミューテックス・セマフォなど)を使ってリソースアクセスをコントロールするのだが、これはこれで難しい問題(デッドロックとかね)があるのである。

JSの世界はシングルスレッドなので、リソース競合は起きない(同時に同じリソースをアクセスすることはない)ため、その分考え方をシンプルにすることができるのが利点である。さらにはコンテキスト・スイッチが発生しないので、本来は同一時間中に裁ける処理数は、マルチスレッドに比べて勝るはず(その代わり、OSが行う部分をJS側で肩代わりする必要がある)である。が、今の時代はCPUもマルチコアでHWで複数スレッドが走る(CPUもマルチスレッド・マルチプロセスをサポートするように進化している)なので、一概にそうとも言えない面もある。

この話の流れだと静的ブログジェネレーターの設計変更はそのような非同期プログラミングを回避するために行う雰囲気である。が、それだけではない。nodeでプログラミングをする限り非同期処理は不可避である。
最初の考えでは処理が遅くてちょっと実用に耐えられそうにないので設計変更することにしたのである。

静的ブログジェネレータは、あるディレクトリに格納されている.mdファイルを読みだして、公開用のディレクトリに.htmlに変換して保存するものである。 当初はディレクトリの検索を非同期かつ再帰的に行いつつ、.htmlファイルの変換も行うようなコードを書いた。最初はpromiseベースであったが途中からasync/awaitで書き直し、見た目同期的なコードとなった。しかし実際の挙動が意図したもの(見た目通り)とならず、数日悩むことになった。その問題は解決したが、今後追加機能などでコードを修正・追加を加えるのがつらそうで結局、 ディレクトリとHTML変換を同時にするのをやめ、「ディレクトリ検索を再帰的に行う部分のみ同期的に行って.mdファイルをリスト化する」部分、「そのリストを使ってHTMLファイルに変換する」部分に分けてコードを書くようにした。

次に、このコードに

  1. 新たに.mdファイルをディレクトリに追加する処理
  2. .mdファイルを更新した時の処理
  3. .mdファイルをディレクトリから削除した時の処理

の処理を追加することにした。上記処理を行うにはディレクトリ内のファイルの追加・削除・変更を検知する必要がある。これを実現するためにnedbに作成したファイルの情報を保存しておき、次回実行した際にその情報を参照しながら追加・削除・変更することにした。このdbがあれば、追加・削除・変更があったファイルのみ変換・削除することもできる。これによって所望のものができたが、DBを全検索してディレクトリ中のファイルと照合する部分でどうしても時間がかかってしまう。データは4000個近くあるので差分を更新するようにしても30秒~1分程度かかってしまうのである。これはどうしたものか。。

よくよく考えると、.mdファイルの格納ディレクトリはgitで管理しているのでディレクトリ内のファイルの差分情報が得られる。わざわざ差分を検出するコードなど書く必要はないのである。そういうわけでgitを使って追加・削除・変更するように設計変更し、今コードを書き換えているところである。