C#で非同期処理を調べるとTaskの記事が多く見受けられます。
docs.microsoft.com
Task自体理解するのが難しく、サンプルコードだけでは理解しきる事は困難です。
さらに読み進めていくと根本にはスレッドが使われている事に気づきます。
初学者にとっては、Taskだけでなくスレッドを読み進めていくのはとても難しい事です。
今回は、スレッドについて例え話を交えて書いていきたいと思います。
最初に
Overcookedと言うゲームを御存じでしょうか?
Overcooked (R) 2 - オーバークック2 -Switch
- 発売日: 2018/11/29
- メディア: Video Game
Overcookedは料理の配膳を題材にしたアクションゲームです。
注文を聞いて、料理作り、お皿に盛ってお客さんに提供し、お皿を洗う…
せわしなくキッチンを動き回るゲームです。
プレイ動画を見ただけで疲れてしまいます。
実際の店もせわしなく作業されているのではないでしょうか?
ちなみに私はラーメンが好きです。
行きつけのラーメン屋も、店主の方は一人でせわしなくラーメンを作っていたのを思い出します。
閑話休題
ラーメン屋が忙しいと書きましたが、実際どのような仕事をしているのでしょうか?
私の記憶を頼りにまとめると次のような作業をされているのではないでしょうか。
スレッドを説明する上で、この作業工程は重要になります。
この作業工程表は平たく言えば、プログラムです。
一般的には「コンピュータに実行させる処理を順番に書き出したもの」ですので、 暴論かもしれません。
考え方としては、処理を順番に書き出したものです。
スレッドとは
先ほどラーメン屋のプログラムを記載しましたが、あくまで工程表で実行結果ではありません。
スレッドとは一連のプロセスを実行する流れの事を言います。
プロセスとは
本来は経過、過程、工程等、一連の流れの事を意味します。
スレッドと意味合いが似ていますし、目的によっては意味合いが変わってきます。
プログラムでは、処理の実行単位を指してプロセスと言います。
ラーメン屋に例えると
「注文を聞く」「麺をゆでる」「スープを作る」などなど、それぞれの作業がプロセスです。
タスク
ここでは、Task クラスではなく概念について触れます。
タスクはプロセスの集まりです。
並んだプロセスはそれぞれ別の事をしているとはいえ、何かしら同じ目的のために実行されているはずです。
プログラム全体ではなく、細分化された目的のまとまりの事を指してタスクと言います。
ラーメン屋を例にすると
「麺をゆでる」「スープを作る」「麺とスープを混ぜる」に共通しているのはラーメンを作るための作業です。
その作業をひっくるめてラーメンを作るというタスクと考えられます。
スレッドの並列処理
処理の開始から終了まで、プログラムの記述順に処理をする事をシングルスレッド と言います。
簡単な処理であればシングルスレッドでも問題ないかもしれません。
しかし、プロセスによっては完了するまで時間がかかるものがあります。
シングルスレッドの場合、実行中のプロセスに時間がかかったとしても終わるまで次のプロセスには進めません。
マルチスレッド
スレッドを分離して、一部のプロセスを別のスレッドで実行します。
スレッドを分ける事で、同時に別のプロセスを実行できます。
また、それぞれのスレッドには名称があります。
大本のスレッドをメインスレッドと言い、
メインスレッドから枝分かれたスレッドをワーカースレッドと言います。
【Tips】スレッドの別名称
スレッドについては記事によって色々な呼び方があります。
メインスレッドは、「プライマリスレッド」と言わる場合があります。
ワーカースレッドは、「サブスレッド」と言わる場合があります。
ラーメン屋に例えると
「スープを作る」と「麺をゆでる」作業に順番があるわけではありません。
そのため、スープを作る片手間で麺をゆでても良いわけです。
店主'と言うワーカースレッドが作成して、2スレッドの構成で処理をします。
ワーカースレッドの処理が終われば、メインスレッドに合流して再びプロセスを続行します。
※ 当たり前ですが人は分身しません。あくまで同時進行のイメージです。
プロセスの通信方式
プログラムを実行すると、プロセスを上から順番に処理します。
次のプロセスに進むには、前のプロセスの完了や応答を待つ必要があります。
プロセスを一つ一つ完了するまで待つ事を同期と言います 。
スレッドの場合は、待機する事でワーカースレッドを同期させることが出来ます。
スレッドを止める事をブロックと言います。
ワーカースレッドの内容によっては、完了を待つ必要が無い(あるいは、終わりが見えないので待てない)処理もあります。
そういった場合、メインスレッドに収束せずワーカースレッドで完結できるように実装します。
このように、プロセスの同期を待たない通信方法を非同期と言います。
同期処理
前項までのように順番に処理をしていく方法です。
メインスレッドは分岐後にブロックして、ワーカースレッドの終了を待ちます。
ラーメン屋にたとえると
例えば、ラーメン餃子セットを提供するとします。
餃子はラーメンに比べて出来上がる時間が短いので、前菜として食べる事が多いです。
餃子を作るワーカースレッドを用意した場合、下記のようにラーメンと餃子がセットになるまで待ってからお客さんに提供します。
非同期処理
メインスレッドはワーカースレッドの終了を待ちません。
ワーカースレッドも独自で終了、あるいは実行し続けます。
ラーメン屋に例えると
同期処理のように、提供を両方出来上がりまで待ってしまうと餃子が冷めてしまいます。
ラーメンを作るタスクに、餃子は必ずしもいりません。
つまり、出来る上がるのを待つ必要がありません。
そのため、ラーメンとは別に出来上がり次第提供しまいます。
これで、美味しいうちに餃子が食べられます。
スレッドプール
今まで1回分の工程について解説してきましたが、実際は複数回同じ工程を繰り返します。
メインスレッドであれば、最初から繰り返し処理で動かし続けられますが、ワーカースレッドは既に終了しているので新しく生成しないといけません。
ワーカースレッドを生成するのも、スレッドを切り替える事もCPUやメモリに負担がかかります。
そこで、生成したスレッドを破棄せず貯めて置き、再利用できるようにします。
この機構をスレッドプールと言います。
実際のプログラムでは必ずしもすべてのスレッドが常時動いているわけではありません。
実行中と待機中のスレッドはそれぞれ別の呼ばれ方をしています。
スレッドプールで管理されるスレッドをバックグラウンドスレッドと言い、
実行中のスレッドをフォアグラウンドスレッドと言います。
ラーメン屋に例えると
ラーメン屋の業務が終わればラーメンをゆでるためのお湯を捨てたり、餃子焼き機の電源を切ります。
しかし、実際のお店は常に次のお客さんが来ます。
お客さんが来るたびに、お湯を沸かしなおしたり、餃子焼き器を温め直したりしないといけません。
これは、時間も労力もかかります。
そのため、前に使ったお湯をそのまま使ったり、餃子焼き器をアイドリングさせたりしてすぐ料理できるようにし ます。
そうすることで、全体コストを下げる事が出来ます。
参考
tech-lab.sios.jp ufcpp.net ascii.jp https://wa3.i-3-i.info/word12453.htm
以上です。
ここまでが、スレッドの話です。
大雑把にまとめましたが、スレッドは作業の一連の流れだという事がお判りいただけたのではないでしょうか。
しかし、現在はスレッドをそのまま使う機会は少ないです。
冒頭にも書きましたがTaskでスレッド処理をする事の方が多いです。
次は、この話を踏まえてタスクについて触れます。