Skip to content

Commit

Permalink
feat(async): 同期処理と非同期処理 (#503)
Browse files Browse the repository at this point in the history
* feat(async): 同期処理と非同期処理

* chore(async): add index

* fix(async): add code meta data

* fix

* fix(async): add log

* feat(async): 非同期処理と例外処理

* fix(async): まとめを変更

* refactor: 同期的なエラーという表現をやめる

* fix

* fix

* fix

* fixup

* fixup

* fix: waitSync関数に変更

* refactor: 重たい処理 => ブロックする処理

* fix

* fixup

* fixup

* fixup

* fixup
  • Loading branch information
azu authored Jun 29, 2018
1 parent 983f8ff commit f44fbae
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 3 deletions.
5 changes: 4 additions & 1 deletion prh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,11 @@ rules:
- expected: HTML要素
patterns:
- DOM要素
- expected: 同期処理
patterns:
- 動機処理

# 一致とマッチ
# 一致とマッチ
## 一致は ===
- expected: $1一致
patterns:
Expand Down
2 changes: 1 addition & 1 deletion source/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
- [関数とthis](./basic/function-this/README.md)
- [クラス](./basic/class/README.md)
- [例外処理](./basic/error-try-catch/README.md)
- Promise
- [非同期処理](./basic/async/README.md)
- [JSON](./basic/json/README.md)
- [Date](./basic/date/README.md)
- [Map/Set](./basic/map-and-set/README.md)
Expand Down
242 changes: 242 additions & 0 deletions source/basic/async/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,245 @@
---
author: azu
---

# 非同期処理 {#async-handling}

この章ではJavaScriptにおける非同期処理について学んで行きます。
非同期処理はJavaScriptにおいてはとても重要な概念です。
また、JavaScriptを扱うブラウザやNode.jsなどにおいて非同期処理のみのAPIも多いため、非同期処理を避けることはできません。
そのため、非同期処理をあつかうためのパターンやPromiseというビルトインオブジェクト、さらにはAsync Functionとよばれる構文的なサポートがあります。

この章では非同期処理とはどのようなものかという話から、非同期処理での例外処理、非同期処理の扱い方を見ていきます。

## 同期処理 {#sync-processing}

多くのプログラミング言語ではコードの評価の仕方として**同期処理**(sync)と**非同期処理**(async)という大きな分類があります。

今まで書いていたコードは**同期処理**と呼ばれているもので、
コードを順番に文と式を評価したらその評価結果がその場で返されます。

同期処理ではコードを順番に処理していき、ひとつの処理が終わるまで次の処理は行いません。
同期処理では実行している処理はひとつだけとなるため、とても直感的な動作となります。

一方、同期的にブロックする処理が行われていた場合には問題があります。
同期処理ではひとつの処理が終わるまで次の処理を行うことができないためです。

次のコードの`blockTime`関数は指定した`timeout`ミリ秒だけ無限ループを行い同期的にブロックする処理です。
この`blockTime`関数を呼び出すと、指定時間経過するまで次の処理(次の行)が呼ばれません。

{{book.console}}
```js
// 指定した`timeout`ミリ秒経過するまで同期的にブロックする関数
function blockTime(timeout) {
const startTime = Date.now();
// `timeout`ミリ秒経過するまで無限ループをする
while (true) {
const diffTime = Date.now() - startTime;
if (diffTime >= timeout) {
return; // 指定時間経過したら関数の実行を終了
}
}
}
console.log("処理を開始");
blockTime(3000); // 他の処理を3000ミリ秒(3秒間)ブロックする
console.log("この行が呼ばれるまで処理が3秒間ブロックされる");
```

このような同期的にブロックするは、ブラウザでは大きな問題となります。
なぜなら、JavaScriptは基本的にブラウザのメインスレッド(UIスレッドとも呼ばれる)で実行されるためです。
そのため、JavaScriptで同期的にブロックする処理を行うと他の処理ができなくなるため、画面がフリーズしたような体感を与えてしまいます。

さきほどの例では3秒間も処理をブロックしているため、3秒間スクロールやクリックなどの他の操作が効かないといった悪影響がでます。

## 非同期処理 {#async-processing}

非同期処理は、コードを順番に文と式を評価したら処理は開始されますが、その評価結果を返しません。
(処理が開始されたことを表すオブジェクトなどを返すことはありますが、最終的な評価結果はすぐには手に入りません)

また非同期処理はコードを順番に処理していきますが、ひとつの非同期処理が終わるのを待たずに次の処理を評価します。
つまり、非同期処理では同時に実行している処理は複数あります。

JavaScriptにおいて代表的な非同期処理を行う関数として`setTimeout`関数があります。
`setTimeout`関数は`delay`ミリ秒後に、`コールバック関数`を呼び出すようにタイマーへ登録する非同期処理です。

<!-- doctest:disable -->

```js
setTimeout(コールバック関数, delay);
```

次のコードでは`setTimeout`関数を使い10ミリ秒後に同期的にブロックを行います。
`setTimeout`関数でタイマーに登録したコールバック関数は非同期的なタイミングで呼ばれます。
そのため`setTimeout`関数の次の行に書かれている同期的処理は、非同期処理よりも先に実行されます。

{{book.console}}
```js
// 指定した`timeout`ミリ秒経過するまで同期的にブロックする関数
function blockTime(timeout) {
const startTime = Date.now();
while (true) {
const diffTime = Date.now() - startTime;
if (diffTime >= timeout) {
return; // 指定時間経過したら関数の実行を終了
}
}
}

console.log("1. setTimeoutのコールバック関数を10ミリ秒後に実行します");
setTimeout(() => {
console.log("3. ブロックする処理を開始します");
blockTime(3000); // 他の処理を3秒間ブロックする
console.log("4. ブロックする処理が完了しました");
}, 10);
// ブロックする処理は非同期なタイミングで呼び出されるので、次の行が先に実行される
console.log("2. 同期的な処理を実行します");
```

このコードを実行した結果のコンソールログは次のようになります。

1. setTimeoutのコールバック関数を10ミリ秒後に実行します
2. 同期的な処理を実行します
3. ブロックする処理を開始します
3. ブロックする処理が完了しました

このように、非同期処理(`setTimeout`のコールバック関数)は、コードの見た目上の並びとは異なる順番で実行されることがわかります。

## JavaScriptはメインスレッドで実行される {#JavaScript-and-main-thread}

ブラウザにおいて、JavaScriptはメインスレッドで実行されます。
メインスレッドはUIスレッドとも呼ばれ、重たいJavaScriptの処理はメインスレッドで実行する他の処理(画面の更新など)をブロックする問題について紹介しました。(ECMAScriptの仕様として規定されているわけではないため、すべてがメインスレッドで実行されているわけではありません)

非同期処理は名前から考えるとメインスレッド以外で実行されるように見えますが、
基本的には非同期処理も同期処理と同じようにメインスレッドで実行されます。
このセクションでは非同期処理がどのようにメインスレッドで実行されているかを簡潔に見ていきます。

次のコードは、`setTimeout`関数でタイマーに登録したコールバック関数が呼ばれるまで、実際にどの程度の時間がかかったかを計測しています。
また、`setTimeout`関数でタイマーに登録した次の行で同期的にブロックする処理を実行しています。

非同期処理(コールバック関数)がメインスレッド以外のスレッドで実行されるならば、
この非同期処理はメインスレッドでの同期的にブロックする処理の影響を受けないはずです。
しかし、実際にはこの非同期処理もメインスレッドで実行された同期的にブロックする処理の影響を受けます。

次のコードを実行すると`setTimeout`関数で登録したコールバック関数は、タイマーに登録した時間(10ミリ秒後)よりも大きく遅れてが呼び出されます。

{{book.console}}
```js
// 指定した`timeout`ミリ秒経過するまで同期的にブロックする関数
function blockTime(timeout) {
const startTime = Date.now();
while (true) {
const diffTime = Date.now() - startTime;
if (diffTime >= timeout) {
return; // 指定時間経過したら関数の実行を終了
}
}
}

const startTime = Date.now();
// 10ミリ秒後にコールバック関数を呼び出すようにタイマーに登録する
setTimeout(() => {
const endTime = Date.now();
console.log(`非同期処理のコールバックが呼ばれるまで${endTime - startTime}ミリ秒かかりました`);
}, 10);
console.log("ブロックする処理を開始します");
blockTime(3000); // 3秒間処理をブロックする
console.log("ブロックする処理が完了しました");
```

多くの環境では、このときの非同期処理のコールバックが呼ばれるまでは3000ミリ秒以上かかります。
このように**非同期処理****同期処理**の影響を受けることからも同じスレッドで実行されていることがわかります。

JavaScriptでは一部の例外を除き非同期処理が**並行処理(concurrent)**として扱われます。
並行処理とは、処理を一定の単位ごとに分けて処理を切り替えながら実行することです。

ECMAScriptの仕様では**JobQueue**と呼ばれるキューで後で行うタスクが管理されています。
次に処理するタスクをキューから1つ取り出し、タスクの処理が終わったら次のタスクを取り出りだすというのを繰り返してプログラムを評価しています。

同期処理では、キューにタスクを追加せずに現在ある処理を次々と処理しています。

- [ ] 同期処理のキューの図

一方の非同期処理では、キューのタスクを追加だけして、キューからタスクを取り出して実行するのは非同期で処理します。
`setTimeout`関数でタスク(コールバック関数)をキューへ追加し、指定時間後にタスクを取り出して処理します(コールバック関数を呼び出す)。
キューへ追加した非同期のタスクを取り出す前に同期的にブロックする処理がある場合は、ブロックする処理が終わってから非同期のタスク(コールバック関数)を取り出して実行します。

- [ ] 非同期処理のキューの図

これによって、非同期処理のタスクが同期的なブロックする処理によって実行が遅れるという現象を引き起こします。
そのためJavaScriptの非同期処理も基本的には1つのメインスレッドで処理されていると考えても間違いよいでしょう。
これは、`setTimeout`関数のコールバック関数から外側のスコープのデータへのアクセス方法に制限がないことからもわかります。
もし、非同期処理が別スレッドで行われるならば自由なデータへのアクセスは競合状態(レースコンディション)を引き起こしてしまうためです。

ただし、非同期処理の中にもメインスレッドとは別のスレッドで実行できるAPIが実行環境によっては存在します。
たとえばブラウザでは[Web Worker][] APIを使いメインスレッド以外でJavaScriptを実行できため、非同期処理を**並列処理(Parallel)**できます。並列処理とは、排他的に複数の処理を同時に実行することです。

Web WorkerでのJavaScriptはメインスレッドのJavaScriptとは異なるスレッドで実行されるため、お互いに同期的なブロックする処理の影響を受けにくくなります。
ただし、Web Workerとメインスレッドでのデータのやり取りには`postMessage`メソッドを利用する必要があります。そのため、`setTimeout`関数のコールバック関数とは異なりデータへのアクセス方法にも制限がつきます。

このように、非同期処理のすべてをひとくくりにはできませんが、基本的な非同期処理(タイマーなど)はメインスレッドで実行されているという性質を知ることは大切です。JavaScriptの大部分の**非同期処理****非同期的なタイミングで実行される処理**である理解しておく必要があります。

## 非同期処理と例外処理 {#async-processing-and-error-handling}

非同期処理は処理の流れが同期処理とは異なることについて紹介しました。
これは非同期処理における**例外処理**においても大きな影響を与えます。

同期処理では、`try...catch`構文を使うことで同期的に発生した例外はキャッチできます。(詳細は「[例外処理][]」の章を参照)

{{book.console}}
```js
try {
throw new Error("同期的なエラー");
} catch (error) {
console.log("同期的なエラーをキャッチできる");
}
console.log("この文は実行されます");
```

非同期処理では、`try...catch`構文を使っても非同期的に発生した例外をキャッチできません。
次のコードでは、10ミリ秒後に非同期的なエラーを発生させています。
しかし、`try...catch`構文では次のような非同期エラーをキャッチすることはできません。

{{book.console}}
<!-- doctest: Error -->
```js
try {
setTimeout(() => {
throw new Error("非同期的なエラー");
}, 10);
} catch (error) {
console.log("非同期手なエラーはキャッチできない");
}
console.log("この文は実行されます");
```

`try`ブロックはそのブロック内で発生した例外をキャッチする構文です。
しかし、`setTimeout`関数で登録されたコールバック関数が実際に実行され例外を投げるのは、すべての同期処理が終わった後となります。
つまり、`try`ブロックで例外が発生しうるとマークした**範囲外**で例外が発生します。

そのため、`setTimeout`関数のコールバック関数における例外は、次のようにコールバック関数内で同期的なエラーとしてキャッチする必要があります。

{{book.console}}
```js
// 非同期処理の外
setTimeout(() => {
// 非同期処理の中
try {
throw new Error("エラー");
} catch (error) {
console.log("エラーをキャッチできる");
}
}, 10);
console.log("この文は実行されます");
```

このようにコールバック関数内でエラーをキャッチはできますが、**非同期処理の外**からは**非同期処理の中**で例外が発生したかは分かりません。
そのため、**非同期処理の中**で例外を発生した場合に、その例外を**非同期処理の外**へ伝える方法が必要です。

この非同期処理で発生した例外の扱い方についてはさまざまなパターンがあります。
この章では主要な非同期処理と例外の扱い方としてエラーファーストコールバック、Promise、Async Functionの3つを見ていきます。
現実のコードではすべてのパターンが実用的です。そのため、非同期処理の選択肢を増やす意味でも理解することは重要です。


[文と式]: ../statement-expression/README.md
[例外処理]: ../error-try-catch/README.md
[Web Worker]: https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Using_web_workers
3 changes: 2 additions & 1 deletion test/markdown-doc-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ describe("doctest:md", function() {
} else {
strictEval(poweredCode, {
require,
console
console,
setTimeout
});
}
} catch (error) {
Expand Down

0 comments on commit f44fbae

Please sign in to comment.