多分わかりやすいC#の非同期処理その1 〜 ThreadとTask 〜

こんにちは、サイオステクノロジー技術部 武井です。今回は、C#を学び始めた人たちが一番始めに当たる関門であろう非同期処理ついて解説したいと思います。2回シリーズの第1回目はThreadとTaskです。

非同期処理

非同期処理とは何であるかを説明する前に、まず同期処理のことをお話したいと思います。

同期処理とは、結果が返ってくるまで待つ処理のことです。例えば、とあるメソッドmethodAがあったとします。このmethodAは文字列hogeを返すとします。このmethodAを実行して、hogeが返ってくるまで次の処理に進まず、待っているのが同期処理です。

かたや非同期処理とは、methodAを実行後、hogeが返ってくるのを待たずに次の処理に進んでしまいます。

非同期処理を活用するシーンの代表格は、やはりUI処理でしょう。例えば、何らかのボタンをクリックして処理が終わるまでにUIが固まって、他の処理ができなくなってしまうようでは、ユーザービリティはガタ落ちですよね。例えばWebブラウザで、ファイルをダウンロードしたときに、Webブラウザ自体が固まってしまうようなものだった場合、そんなWebブラウザ使いたいと思う奇特な人いないでしょう。

そこで、非同期処理の出番なのです。ボタンをクリックしたときにUIスレッドとは別のスレッドを起動して、UIスレッドを専有しないことで、ファイルダウンロード中でもUIが固まらないようにします。

でも、ところがどっこい、非同期処理って難しいです。ちょっと間違うと、スレッドセーフでないプログラム作ってデータ破壊したり、デッドロック起こしたり、例外が補足出来なかったり、結果を受け取るのがめんどくさかったり、、、。

そんな非同期処理を誰でも簡単にできるようにしようというのが、Taskなのです。これらを使えば、まるで同期処理を書いているような感覚で、非同期処理を実現出来ます。ただ、これらの概念って理解が難しいんですよね。そんな壁にぶつかっている人たちに本記事がお役に立てれば幸いに存じます。

そしてタイトルからもわかりますように、その1とあるので、続きを書く予定です。その2では、もっとわかりにくいasync、awaitについて、多分わかりやすく書こうかなと思っています。

レガシーな非同期処理Thread

Task、async、awaitを説明する前に、まず、レガシーな非同期処理の代表格Threadについてご説明して、その上でThreadとTask、async、awaitの違いをご説明します。

今のこのご時世、これから新規に非同期処理を作るときにThreadを使うことはあまりないかと思いますが、まず新しい概念を説明するときは、今までのレガシーなやり方と比較するとわかりやすいと思うので、ちょっと説明させて頂ければと思います。

以下のコードは、メインスレッドとは別のスレッドで起動したHeavyMethod1で重い処理をしながら、メインスレッドで別の重い処理HeavyMethod2を実行します。処理はThread.Sleepで擬似的に実現しております。非同期処理の説明あるあるですね。

結果は以下のようになります。

Threadでの処理をタスクに置き換える

先程のThreadで実施した処理をTaskで置き換えてみます。何で、わざわざTaskで置き換えなければ行けないかと言うと、TaskではThreadで実現出来ない以下のことが実現できるためです。

  • 非同期で実施した処理の状態(実行中、完了、キャンセル、エラー)を知ることができる
  • 例外を補足することができる
  • 非同期処理の実行順序を制御できる

つまり、Taskは非同期処理を実現するための、Threadに変わる新しい方法と思って頂ければよいと思います。

では、先程のThreadで実施した処理をTaskで置き換えたコードは以下のとおりです。

単純にThreadの部分をTaskに置き換えただけですので、当然Taskによる効果はほとんど感じられないと思います。

もちろん結果は先程と同じです。

次からはユースケース別に、Threadで実装した場合とTaskで実装した場合を比べて見たいと思います。

結果を受け取りたい

非同期処理で結果を受け取りたい場合を考えてみます。hogeという文字列を返すHeavyMethodを実行し、その戻り値であるhogeを受け取ってみたいと思います。

Threadの場合

Threadの場合は以下のコードになります。

結果は以下のようになります。

Taskの場合

Taskを使った場合、以下のようなコードになります。

Threadに比べてTaskを使った方式だと、より直感的に戻り値を取得できるようになった気がしませんでしょうか?Threadを使った場合は、わざわざメインスレッドでresultという変数を定義して、スレッドの中でそれを使っています。しかもJoinメソッドでスレッドの終了を待機しています。いいかにもスレッドであることを意識したコードです。

それに比べてTaskの場合は、TaskのResultプロパティからサクッと取得することが出来ます。Resultプロパティを使うだけで、Threadを使うことによって実施していた以下の2つの処理をしなくてすむようになります。

  • メインスレッドでresult変数をあらかじめ定義する。
  • スレッドが終了するまでJoinメソッドで待機する。

これらの処理は、Taskの場合は、Resultプロパティを使えば、全部やってくれます。

つまり、Taskは、使い人があまりスレッドを意識することなく、同期的なコードに近いコードで非同期を実現できるものです。

例外を補足したい

ThreadとTaskの違いのもう一つの例として、例外の補足の容易性が挙げられます。スレッドの中で発生した例外を補足する場合は考えてみます。

Threadの場合

スレッドの中で例外を発生させたコードです。

結果は以下のようになります。

スレッドの中の例外を握りつぶしてしまっていて、メインスレッドは何事もなかったかのように終わってしまいました。これでは、困りますね。

Taskの場合

Taskの場合では、どうでしょうか?

結果は以下のようになります。

きちんとスレッドの中で発生した例外をメインスレッドがキャッチできました。

ここにおいても、Taskを使った場合は、スレッドを意識することなく、同期的なフローと同じような処理で例外をキャッチできました。

複数のスレッドの終了を待ちたい

複数のスレッドを並行で実行させて、それらのスレッドの結果を処理したいということありませんでしょうか?

Taskを使うとこういうこともラクチンに出来ます。

ここでは、1の整数値を返すHeavyMethod1と、2の整数値を返すHeavyMethod2があり、それらを並行で実行させて、それらの結果をすべて足して、コンソールに3を出力するケースを考えてみます。

Threadの場合

横着してすみません(´・ω・`)ちょっとThreadでこの処理を書く方法をぱっと思いつかなったのですが、相当めんどくさい処理になりそうなことは想像に難くないのではないでのでしょうか?

気を取り直して、Taskで処理する場合のコードに移りましょう。

Taskの場合

Taskの場合は、WhenAllメソッドというものを使うと、複数のスレッドの終了を待つことができます。

結果は以下のようになります。想定どおりですね。

こんな複雑な処理もTaskを使うと簡単に処理できます。

TaskはThreadの進化系です。複雑だった同期処理をすごい簡単にしてくれます。ありがたいですね。

まとめ

Taskを使うと、今までThreadでやっていたことがすごくとてもシンプルにできることがご理解いただけたかと思います。

次はasync、awaitについて書きます。

多分わかりやすいC#の非同期処理その2 〜 asyncとawait 〜

>> 雑誌等の執筆依頼を受付しております。
   ご希望の方はお気軽にお問い合わせください!

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

6人がこの投稿は役に立ったと言っています。

コメント投稿

メールアドレスは表示されません。


*