はじめに
最近TypeScriptの勉強をしており、TypeScriptを用いた画面の表示手順で全体観を掴みつつ、コーディングをしていくにあたって、TypeScriptには必須の型宣言方法の種類についてまとめてきました。
さらに今回勉強したTypeScriptでよく使う非同期処理についてですが、勉強する前はそもそもよくわかっていないのは前提として、非同期処理って何の利点があるのか?と疑問にずっと思っていました。そこで自分なりに勉強してみてある程度理解できたので、初学の自分でも理解したレベルに落とし込んで、解説できればと思います。
参考記事:https://qiita.com/soarflat/items/1a9613e023200bbebcb3
非同期処理の良さとは...?
はじめに、非同期処理と対の言葉に「同期処理」というものがあります。それぞれの言葉の意味として下記になります。
用語 | 意味 |
---|---|
同期処理 | タスクを順番に実施する処理方法 |
非同期処理 | タスクを並行して実施する処理方法 |
そこで実際にコーディングをするにあたって、具体例として普段の日常生活でも行うような下記のタスクを考えます。
タスク | 所要時間 | 条件 |
---|---|---|
部屋掃除 | 30分 | |
米を炊く | 50分 | |
味噌汁を作る | 10分 | |
食事をする | 30分 | 部屋掃除,米を炊く,味噌汁を作るタスクが完了している |
実際の流れを図に示すと下記のようになります。
この図を見てもらうと、同期処理では、1つ1つタスクを順番に行なっていく必要があるので、今回のタスクの場合は、合計時間が120分とそれだけ時間を要してしまいます。また、大規模なシステムになってくると処理が増えてくることから、1つ1つ順番に処理を実行していく同期処理の場合、処理パフォーマンスの低下にも繋がりかねません。
それに対して非同期処理では、並行してタスクを進めていくことができます。今回のタスクについても、米を炊いている間に、部屋掃除と味噌汁作りを進めることができるので、これらのタスクを同時並行させます。その場合、合計時間が80分と約33%時間を短縮できます。この非同期処理の利点は大規模なシステム処理となっても、処理が効率よく回ることから、処理パフォーマンスも最適化されます。
実装してみる
上記で解説した同期処理と非同期処理のタスクですが、実際にコーディングをしてみると、以下のようになります。
※注意※
処理名称についてですが、ここでは「同期処理」「同期的処理」「非同期処理」の3種類を列挙しています。
処理名称 | 内容 |
---|---|
同期処理 | async(非同期処理メソッド)を定義せずに書く処理方法 |
同期的処理 | asyncを定義して、内部で同期処理を実行する方法 |
非同期処理 | asyncを定義して、内部で非同期処理を実行する方法 |
「同期的処理」の部分を、公式ドキュメントや他記事では同期処理や非同期処理と混在して書いていたように感じたので、本記事では「同期的処理」と名称を統一しています(個人的にはこの名称部分でかなり混乱してしまいました。。)。
今回は実装にて時間計測を用いるので、asyncを用いた「同期的処理」「非同期処理」について解説していきます。
同期的処理
// 計測
const elapsedTime = (start: number, end: number) : string => {
return (end-start).toFixed(0)
}
// タスク消化
const doTask = (task: string, time: number) : Promise<string> => {
return new Promise(resolve => {
setTimeout(() => {
resolve(task + ": " + String(time) + "分");
}, time);
})
}
// 順番にタスク消化(同期的処理)
const singleTasks = async () => {
console.log("===順番タスク開始===")
const start: number = performance.now()
let end: number
await doTask("部屋掃除をする", 30)
end = performance.now()
console.log(elapsedTime(start,end) + "分") // 32分
await doTask("米を炊く", 50)
end = performance.now()
console.log(elapsedTime(start,end) + "分") // 89分
await doTask("味噌汁を作る", 10)
end = performance.now()
console.log(elapsedTime(start,end) + "分") // 101分
await doTask("食事をする", 30)
end = performance.now()
console.log(elapsedTime(start,end) + "分") // 135分
}
singleTasks()
実際に同期的処理で実行してみると、135分(実測では135ms)の時間を要していることが確認できます(理論上は120分ですが、他の時間計測等の処理も含まれているため、理論的な数値よりは若干多めに出ています)。
非同期処理メソッドの記法についてですが、現在では簡単に非同期処理メソッドを書ける観点から、async/awaitを用いた記法が主流となっています。他にもコールバック関数やawaitを使用しない記法もあるのですが、こちらは現在の記法よりも可読性が落ちるので非推奨となっています。具体的な概念や実装方法についてはこちらの記事をご覧ください。
またasync/awaitを用いた記法についてですが、asyncにて非同期処理の実行が可能な状態にして、awaitを用いて同期処理を実行します。async/awaitの記法の詳細を知りたい場合は、こちらの記事で解説していただいてますので、ぜひご覧ください。しかしこのままだとasyncの恩恵を受けられていないので、次に下記コードで非同期処理を実現していきます。
非同期処理
// 計測
const elapsedTime = (start: number, end: number) : string => {
return (end-start).toFixed(0)
}
// タスク消化
const doTask = (task: string, time: number) : Promise<string> => {
return new Promise(resolve => {
setTimeout(() => {
resolve(task + ": " + String(time) + "分");
}, time);
})
}
// 並列にタスク消化(非同期処理)
const multiTasks = async () => {
console.log("===並列タスク開始===")
const start: number = performance.now()
let end: number
await Promise.all([
doTask("部屋掃除をする", 30).then(_ => doTask("味噌汁を作る", 10)),
doTask("米を炊く", 50)
])
end = performance.now()
console.log(elapsedTime(start,end) + "分") // 54分
await doTask("食事をする", 30)
end = performance.now()
console.log(elapsedTime(start,end) + "分") // 87分
}
multiTasks()
非同期処理を適用すると、87分(実測では87ms)となっていることが確認できます(理論上は80分ですが、他の時間計測等の処理も含まれているため、理論的な数値よりは若干多めに出ています)。非同期処理はawait内にPromise.all
を定義することで実現できます。Promise.all内で順列の処理を行うためには、then
で接続します。これにより、同期的処理(実際は同期処理)と比較して、約50分処理時間を短縮できていることが実装上でも確認できます。
最後に
今回は、非同期処理のメリットや実装面について解説していきました。勉強する前までは、特に「同期的処理」の概念について記事によって書いてあることが異なっていたため、理解に戸惑った部分もあったのですが、この記事を執筆したことによってだいぶ整理ができたと思っています。TypeScriptの基本的事項については、型定義や非同期処理が学べたことより、今後はTypeScriptを用いた実装の経験をこれから重ねていきたいと思います。
最後に、TypeScriptの実行環境のURLを添付しておくので、上記のソースコードを試してみたい場合は、ぜひコピペして確認してみてください。
TypeScript実行環境:https://www.typescriptlang.org/play