Node.js/Typescript で async-lock を使って非同期処理を同期処理風に記述


Node.js でファイルアクセスや DB アクセスなどの非同期処理を連続して記述しようとすると、非同期処理の callback がネストしてプログラムが読みにくくなってしまいます。
（書く方もつらい。。。）

そこで、async-lock を使って連続する非同期処理を同一のロック変数でロックすることで、ネストを深くせずに同期処理風に記述できないか、試してみました。

# 非同期処理の記述
ここでは以下のような処理を順次実行する場合を考えます。
1. ファイル1 を読み込み、変数1 に保存
2. ファイル2 を読み込み、変数2 に保存
3. ファイル3 へ変数1 の値を書き込み
4. ファイル4 へ変数2 の値を書き込み

上記の処理内容を readFile、writeFile で callback を記述すると、以下のようにネストが深いプログラムになってしまいます。

~~~javascript
fs.readFile(file1, (err, data1) => { // 処理1
  fs.readFile(file2, (err, data2) => { // 処理2
    fs.writeFile(file3, data1, (err) => { // 処理3
      fs.writeFile(file4, data2, (err) => { // 処理4
      });
    });
  });
});
~~~

※ファイル入出力に限らず非同期処理の記述方法を検証するため、読み書き処理で readFileSync()、writeFileSync() は使わないものとします。

# async-lock による排他制御
async-lock は排他制御を行うためのライブラリです。
https://www.npmjs.com/package/async-lock
サンプルとして、以下のような例が紹介されています。

~~~javascript
var AsyncLock = require('async-lock');
var lock = new AsyncLock();

lock.acquire(key, function(done) {
	// async work
	done(err, ret);
}, function(err, ret) {
	// lock released
}, opts);
~~~

`key` がロック変数で、`// async work` に排他する処理を記述します。


# async-lock を利用した非同期処理の記述
例に挙げた4つの非同期処理を逐次実行するようにするため、async-lock で同一のロック変数を使って排他制御することで、各非同期処理を逐次実行するようにプログラムを記述してみます。

~~~javascript
import * as fs from 'fs';
import AsyncLock from 'async-lock';

function main() {
  let var_1 = '--';
  let var_2 = '--';

  const lock = new AsyncLock();

  // file1.txt から読み込み
  lock.acquire('sync', (done: any) => {
    const file = 'file1.txt';
    console.log('before read ' + file);
    fs.readFile(file, 'utf-8', (err, data) => {
      if (err) {
        console.log('error: failed to read ' + file);
      } else {
        var_1 = data;
        console.log(var_1);
        console.log('after read ' + file);
      }
      done();
    });
  });

  // file2 から読み込み
  lock.acquire('sync', (done: any) => {
    const file = 'file2.txt';
    console.log('before read ' + file);
    fs.readFile(file, 'utf-8', (err, data) => {
      if (err) {
        console.log('error: failed to ' + file);
      } else {
        var_2 = data;
        console.log(var_2);
        console.log('after read ' + file);
      }
      done();
    });
  });

  // file3 へ書き込み
  lock.acquire('sync', (done: any) => {
    const file = 'file3.txt';
    console.log('before write to ' + file);
    fs.writeFile(file, var_1, (err: any) => {
      if (err) {
        console.log('error: failed to ' + file);
      } else {
        console.log('after write to '+ file);
      }
      done();
    });
  });

  // file4 へ書き込み
  lock.acquire('sync', (done: any) => {
    const file = 'file4.txt';
    console.log('before write to ' + file);
    fs.writeFile(file, var_2, (err: any) => {
      if (err) {
        console.log('error: failed to ' + file);
      } else {
        console.log('after write to ' + file);
      }
      done();
    });
  });

  // 変数の値を出力
  lock.acquire('sync', (done: any) => {
    console.log('var_1');
    console.log(var_1);

    console.log('var_2');
    console.log(var_2);
    done();

    return 0;
  });
}

console.log('before main');
main();
console.log('after main');
~~~

上記のプログラムを実行した結果は以下の通りです。（※はコメント）

~~~
before main
before read file1.txt		※file1 から読み込み
after main					※★ 
abcdef

after read file1.txt
before read file2.txt		※file2 から読み込み
read: 123456

after read file2.txt
before write to file3.txt	※file3 へ書き込み
write: abcdef

after write to file3.txt
before write to file4.txt	※file4 へ書き込み
after write to file4.txt
var_1: abcdef				※var_1 の内容

var_2: 123456				※var_2 の内容
~~~


プログラムの実行結果を見てみると、
file1 から読み込み　⇒　file2 から読み込み　⇒　file3 へ書き込み　⇒　file4 へ書き込み
の順で実行されていることがわかります。

しかし、★の `after main` は `main()` の実行後に表示されるメッセージで、自分の意図とは異なり、一連のファイル読み書き処理後に実行されずに、すり抜けてしまいました。

# 最後に
async-lock を使うことで、一連の非同期処理を同期処理風に記述することができました。
しかし、意図しないすり抜けが発生しているため、調査してみたいと思います。