きな粉もち.net

.NET関連仕事に携わっています。OSSのソースを読んで気がついたことを中心に呟いたりブログに投稿したりしています。最近はUiPathを使ったRPAも研究中。気軽にフォローやツッコミよろしくおねがいします! Gitはここを使っています https://github.com/kinakomotitti

基礎学習 × .NET Framework の非同期処理を見直してみた × その1

この記事の目的

この記事では、
.NET Frameworkの非同期処理についての基本的なことをまとめること
を目的としています。

また、基本的なことをまとめる中で、
「非同期処理プログラミングを学ぶ上で、どこから手を出していけばよいか」
を自分なりに考え直してみます。

本題

★はじめに

ここでは、「プログラミング.NET Framework 第4版」を読んで学んだことをアウトプットしています。

「その1」ということで、今回は、多数ある実装方法のうち、
Thread、ThreadPool、Taskクラスの概要と、
それらを使って非同期処理を開始するための実装についてまとめます。

さらに、これまで個人的に後回しにしていたAsync/Awaitについてもまとめてみます。

.NET Frameworkの非同期処理概要

I/O バインドのニーズ (ネットワークからのデータの要求、データベースへのアクセスなど) がある場合、非同期プログラミングを利用することになります。 CPU バインドのコードにも、コストのかかる計算の実行など、非同期コードに適したシナリオがあります。

docs.microsoft.com

Microsoft Docsには上記のように紹介されています。
このように、計算処理の実行時間を短縮したり、
GUIアプリケーションで、処理中でも画面を操作不能にしないようにするときにも
非同期処理が必要となる場面が多くあります。

★Threadクラス

Threadクラスは、専用スレッドを作成することができるクラスです。
実行をバックグラウンド・フォアグラウンドで実行するように指定することができたり*1
スレッドの優先度を指定して実行することができたりと、
スレッドに対していろいろな設定を行うことができます。

しかし、ThreadPoolのように、”前に使ったスレッドをつかいまわす”ことがで来ません。
それにより、スレッドを作成するためのコストが毎回かかってくるため、パフォーマンスの面で課題があります。
これらの理由から、Threadクラスでの非同期処理の実装は推奨されていないようです。

そんなThreadクラスを使った実装は以下のようになります。

★ThreadPoolクラス

ThreadPoolクラスを利用して非同期処理を実装すると、CLRで用意されたスレッドプールを利用することができます。
スレッドプールは、以下のような特徴があります。

アプリケーション自身が使用できるスレッドのセットと考えることができます。

  • -

スレッドプールのスレッドがタスクを完了したとき、そのスレッドは破棄されずにスレッドプールに戻され、別の要求に応答するために待機し続けます。

スレッドプールにスレッドがない場合は、スレッドの作成が行われますが、
スレッドがある場合は、あるものを使うため、スレッド作成にかかるコストがかかりません。

ちなみに、スレッドプールのスレッドは規定で、バックグラウンドで実行されます。

そんなThreadPoolクラスを使った実装は以下のようになります。

★Taskクラス

ThreadPoolクラスでは、スレッドの再利用、非同期処理のキャンセル、タイムアウトなど、よく使いそうな機能がサポートされています。
しかし、非同期処理が完了したという通知を呼び出し元にできないという問題があります。
また、処理の結果を呼び出し元に返すこともできません。

それらの問題を解決するために登場したのがTaskクラスです。
Taskクラスを利用することで、ThreadPoolクラスのいいところはそのままに、
呼び出し元に処理(タスク)の完了通知や、結果を返すことができます。
タスクの完了通知が受け取れることで、
タスクの完了を待って、後続の処理を実行することができるようになります。
Taskクラスのこれらの機能は、また別枠で整理していこうと思います(´▽`)

そんなTaskクラスを使った実装は以下のようになります。

★Async/Await

Async/Await修飾子を使った非同期処理の実装はとてもシンプルで、
ほぼ、同期処理の実装と同じように実装することができる。

ただ、シンプルなだけ、内部で行われていることを把握しようとするととても大変です。
参考書には以下のように説明があります。

メソッドをasyncで修飾すると、基本的に、コンパイラはメソッドのコードを、非同期ステートマシンを実装する型に変換します。これによって、スレッドがいくつかのコードを非同期ステートマシン内で実行するようにし、完了に必要なコードすべてを実行することなく制御を戻すことができるようになります。

非同期ステートマシン・・・

わかりそうでわからない・・・

ここが理解できるところを一つのゴールにして今後も調べていきます(=_=)
ここでは、公式のDocsにあった実装時の注意点で気になった内容を確認して、お茶を濁そうと思いますw

async メソッドの本体に await キーワードが含まれないと、何も行われません
これは、忘れてはならない重要なことです。 await が async メソッドの本体で使われていない場合、C# コンパイラは警告を生成しますが、コードは通常のメソッドと同様にコンパイルされて実行されます。 また、非同期メソッドに対して C# コンパイラが生成するステート マシンは何も行わないので、非常に非効率的であることにも注意してください。

docs.microsoft.com

awaitのないメソッドはただの同期処理として扱われるようです。
以下、試してみた実装と結果です。

★結局、どんな時にどれを使えばいいのか

ここにあるだけで、3つの実装方法があります。
結局どれを選択していけばよいのか迷いますが、
実装効率、Docsを参考にすると、非同期処理は、Task、Taskを利用するのが一番だと思います。
また、Taskを利用するにあたり、Async/Await修飾子を利用することで、煩雑になりがちな非同期処理をシンプルに実装することができます。
パフォーマンス問題や、特殊なシナリオを実現する必要があるときだけ、Threadクラスなどを視野に入れていけばよいのかなと思います。

その1、まとめ

非同期処理を実装するための技術として、歴史的にはいろいろなものがありますが、
Taskクラスを利用する非同期処理を把握するのが一番!(のはず)

それ以外は、出てきたときにまた考えますw