Node.jsはシングルスレッド、排他は必要ない

**※タイトルは釣りです。**
この記事はNode.js初心者がasync/await実装をマルチスレッドと誤解して
混乱するのを解消するために書きました。


Node.jsのコードはシングルスレッドで動きます。

(厳密に言うとWorkerとかモジュール内部とかマルチスレッドな部分はありますが、ユーザーが普通に書くコードとしては)

## 結論
- あなたがasyncと書いても、コードが２箇所で同時に実行されることはありません。
- 実行されるコードは常に１箇所のみです。
- awaitと書くとコードの実行が止まって、他のコードに処理が移ることがあります。
- awaitではないコードで、処理が他のコードに切り替わることはありません。
- **あなたが作りたいものによって、排他が必要になることがあります。**


## 質問

外部から通信を受け付けるプログラムを書いています。
通信Aを処理している最中に、通信Bが届いたとき、何が起こりますか？

「通信Aの処理が終わるまで、通信Bの受信処理は実行されない」
「通信Aの処理の最中に、通信Bの受信処理が割り込む」




## 回答

- （作り方によって）どっちもありうる


## awaitの処理が割り込まれない例

```javascript
//時間がかかる処理
async function heavyOperation(){
  console.log('heavyOperation');
  let x = 0;
  for(let i=0;i<10000000000;i++){
      x += i / 10000000000;
  }
}


async function callback(){
  console.log('callback');
}

async function exec(){

  console.log('exec');

  setTimeout(callback,1);

  const startTime = Date.now();
  console.log('heavy operation started.');
  await heavyOperation();
  console.log('heavy operation finished.', Date.now() - startTime, 'ms');

}

exec();
```

```shell
% node settimeout.js
exec
heavy operation started.
heavyOperation
heavy operation finished. 12254 ms
callback
```

heavyOperationの実行には12秒ほどかかっていますが、
setTimeout(1ミリ秒)によるcallback実行は後回しにされました。
関数callbackにasyncがついている/ついていないで違いはありません。



## awaitの処理が割り込まれる例

```javascript
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}


async function callback(){
  console.log('callback');
}

async function exec(){

  console.log('exec');

  setTimeout(callback,1);

  const startTime = Date.now();
  console.log('sleep started.');
 
  await sleep(3000);
  console.log('sleep finished.', Date.now() - startTime, 'ms');

}


exec();
```

```shell
% node sleep.js 
exec
sleep started.
callback
sleep finished. 3000 ms
```

await sleep(1000ミリ秒)の最中にsetTimeoutによる関数callbackの呼び出しが行われています。


つまりawaitする関数の中身によって、他の処理に切り替わるかどうかが変わります。


## 処理が割り込むか、割り込まないか、どうやったらわかるの？

```javascript
const fs = require("fs");

async function readFile１(){
  return (await fs.promises.readFile('./test.txt')).toString();
}

async function readFile2(){
  return fs.readFileSync('./test.txt').toString();
}
```

「よく考えればわかります。処理がイベントキューに積まれることを意識するべきだ！」

とか言っていると見落としがちです。わかりにくいんです。

この例では、readFile1は割り込みますが、readFile2は割り込みません。
readFile1ではawaitを書かないでPromiseのままreturnすることもできるので
中身でawaitを使っているかどうか、は判断として使えません。
fs.promises.readFileはわかりやすい名前ですが、関数名で見分けがつくとも限りません。

async/awaitで説明しましたが、コールバックでも似たような状況
(Concurrent safeではない)が作れます。



## 質問の例に戻ると

通信Aを処理している最中に、通信Bが届いたとき、何が起こりますか？

通信A = ファイルの書き込み
通信B = ファイルの送信

という処理をする場合

```plantuml
クライアント -> サーバー : 通信A(write要求)
サーバー ->> サーバー : asyncファイル書き込み開始

クライアント -> サーバー : 通信B(send要求)
サーバー ->> サーバー : ファイルを送信（でもファイル書き込みが終わってない！）

サーバー ->> サーバー : asyncファイル書き込み完了

```

（そもそも仕様が悪いというのはスルーするとして）
ファイルが存在しなくて送れない、ファイルが古いものを送ってしまう
ということが起こってしまうということです。

関数が割り込みされるかどうか（他の処理が）、


つまりシングルスレッドでも、マルチスレッド程ではないとしても
クリティカルセクション、排他を意識したコーディングが必要になるということです。


## どうすればいいの？

いろんなやり方があります。

1. 通信Aでflag=true（完了時にflag=false）して、通信Bをflag===trueで失敗にする
2. 通信Aでflag=true（完了時にflag=false）して、通信Bでflag===falseになるまで待つ
3. 通信A、Bをキューイングして、キューから処理を実行する
4. [async-lock](https://github.com/rogierschouten/async-lock)を使う

3について想像が難しい場合は、マルチスレッドパターンですが、Active Object、Proactor、Reactor、Worker threadとか調べるといい説明がでてくるかもしれません。


4のasync-lockはスター数253のライブラリで、Lockという名前がついていますがキューのような動作をしていて、lock.acquireはpromiseを返すので、async/awaitな実装と一緒に使いやすいです。


## async-await


```javascript
const AsyncLock = require('async-lock');
const lock = new AsyncLock();


async function sleep(ms) {
  return lock.acquire('mylock', async ()=>{
    await new Promise(resolve => setTimeout(()=>{
      console.log('sleep resolved');
      resolve();
    }, ms));
  });
}


async function callback(){
  return lock.acquire('mylock', async()=>{
    console.log('callback');
  });
}

async function exec(){

  console.log('exec');

  setTimeout(callback,1);

  const startTime = Date.now();
  console.log('sleep started.');
 
  await sleep(3000);
  console.log('sleep finished.', Date.now() - startTime, 'ms');

}


exec();
```

```shell
% node sleep2.js
exec
sleep started.
sleep resolved
callback
sleep finished. 3001 ms
```

'callback'はおおよそ3秒後に表示されます。
sleep関数のlockを抜けた直後にcallback関数のlock内処理が実行されます。
`sleep finished`より先に`callback`が表示されることに注意して下さい。

- lock.acquireは、他のlock.acquireと同時に実行されないことが保証できる
- lock.acquireを呼んだ順に実行される



## 注意すること

```javascript
return lock.acquire('mylock', async()=>{
  return lock.acquire('mylock', async()=>{
    console.log("hi");
  });
});
```

このように入れ子になると、デッドロックのように止まって何も実行されなくなります。
関数を跨いで入れ子になる場合でも同様なので注意が必要です。
async-lockにはreentrantなlockも使えますが、個人的には特殊なケース以外では使わないように
使い方で注意したほうがいいでしょう。