さて、前回のエントリでは、Windows フォーム内部におけるスレッドの構成や、メッセージループの働きなどについて解説しました。中でも重要なこととして、以下のようなキーポイントがありました。
- UI スレッド上で、長時間処理を動かしてはならない。
長時間処理は、マニュアルスレッドやプールスレッドなどの、他のスレッドに切り出す。 - UI スレッド以外から、UI コントロールを触ってはいけない。
マニュアルスレッドやプールスレッド上から、UI コントロールを読み書き・操作してはいけない。
上記の 2 つの重要ルールについて、Part 2~4 にてより実践的な解説を行っていきます。
- Part 2. タスクスレッドの起動方法
まず、マニュアルスレッドやスレッドプールの起動方法について解説します。 - Part 3. タスクスレッドと UI の協調動作
マニュアルスレッドやプールスレッドから UI コントロールを操作したり読み書きしたりすることはできないため、その回避方法について解説します。 - Part 4. Visual Studio を使ったマルチスレッドアプリケーション開発
上記 Part 2, 3 の作業を簡素化するために用意されている、Visual Studio の機能について解説します。
まず本エントリでは、UI スレッドから切り離した処理を動かすために利用するマニュアルスレッドやプールスレッドのことを、タスクスレッドと呼ぶことにし、その作成方法について解説します。
- デリゲートとは何か
- マニュアルスレッドの新規作成
- スレッドプールへのワークアイテムの追加
- 非同期デリゲートの利用
- タイマの利用
なお、以降の説明では様々なスレッドの起動方法を解説していきますが、突き詰めると、タスクを動かすスレッドには、マニュアルスレッドかプールスレッドかのどちらかを使っています。ただ、その起動方法が様々な種類がある、というだけの話ですので、見かけの多様性に惑わされず、しっかり学習していただければと思います。(今回はサンプルらしいサンプルはないですが、一応くっつけておきます。)
では、順番に解説していきます。
[デリゲートとは何か]
マニュアルスレッドやプールスレッドの起動処理を記述する上で欠かせない技術のひとつが、デリゲートです。デリゲートとは、オブジェクトに対して、「関数や処理ロジック」を引数として渡す際に利用される技術であり、.NET Framework の基盤技術の一つになっています。
そもそも「関数」や「処理」を引き渡すイメージがわかない、という方も多いと思いますので、まずここで簡単に解説します。以降の解説は、スレッディングの話からはちょっとそれますが、非常に重要なので必ず理解してください。(※ すでにデリゲートをご存じの方は、この項目は飛ばして先へ進んでください)
まず、なぜ「関数」を引数として引き渡す必要があるのかを理解するために、「コレクションから、ある条件を満たすものだけを抽出する処理」を考えてみることにします。例えば、List<int>型(動的配列)に含まれる整数値から、3で割り切れるものだけを取り出す処理を行う場合、おそらく多くの方は以下のようなコードを記述すると思います。
1: List<int> data1 = new List<int>();
2: for (int i = 0; i < 100; i++) data1.Add(i);
3:
4: // 自力抽出方式
5: List<int> data2 = new List<int>();
6: foreach (int i in data1)
7: {
8: if (i % 3 == 0)
9: {
10: data2.Add(i);
11: }
12: }
13:
14: foreach (int i in data2) Console.WriteLine(i);
この処理方式は、イラストであらわすと次のように示されます。
この方式は、「ユーザーがコレクション内のデータを1つずつ取り出しては吟味し、手作業で移し変えていく」ようなモデルです。もちろん、この処理自体は正しく動作するのですが、そもそも「何らかの条件に基づいてデータの抽出を行う」という処理自体、非常によく出てくる処理です。
そこで、以下のようなモデルを取ることができないか否かを考えてみます。
つまり、List<int> 型のコレクションに対して、「3で割り切れるか否かを確認するロジック」(=抽出条件のロジック)を外部から与えて、これに基づいて、コレクションクラス自身がデータを自動抽出するようなモデルで実装できないか、と考えるわけです。
実は、.NET Framework の「デリゲート」と呼ばれる仕組みを利用すると、これが実現できます。具体的には、以下の通りです。
- List<int> 型には .FindAll() というメソッドが備わっている。
- このメソッドには、引数として、関数(ロジック)そのものを渡すことができる。
実装方法を以下に示します。
- まず、CheckData() 関数(引き渡すことになる抽出条件判定関数)を作成しておく。
- これを「デリゲート」と呼ばれるオブジェクト(ここではPredicateオブジェクト)にラッピングして 、引数として .FindAll() メソッドに引き渡す。
このようにすると、List<int> 型コレクション(data1)自身が、渡されたロジックに基づいて抽出処理を行い、抽出結果を返します。
1: static void Main(string[] args)
2: {
3: List<int> data1 = new List<int>();
4: for (int i = 0; i < 100; i++) data1.Add(i);
5:
6: // デリゲート(関数ポインタ)方式
7: List<int> data2 = data1.FindAll(new Predicate<int>(CheckData));
8:
9: foreach (int i in data2) Console.WriteLine(i);
10: }
11:
12: static bool CheckData(int i)
13: {
14: return (i % 3 == 0);
15: }
ここで重要なのは、以下のポイントです。
- 本来、引数として渡せるものは、文字列や数値といった「具体的なモノ」。
- しかし、デリゲートのインスタンスでラッピングすると、処理(=関数)を引き渡すことができる。
また、デリゲートで重要なもう一つのポイントは、あるデリゲートクラスがラッピングできる関数は、そのデリゲートが定義している引数/戻り値と完全に一致していなければならない、という点です。上記のサンプルの場合、Predicate<int> というデリゲートでラッピングできる関数は、int 引数ひとつを取り、bool 型を返すような関数に限られています。引数や戻り値の型などがひとつでもずれていると、そのデリゲートでのラッピングはできませんので、注意してください。
※ (注意) 上記のコードでは少し簡略化して書いていますが、デリゲートインスタンス(関数をラッピングしたもの)はそれ自体を変数によって保持することができます。例えば、上記のコードは以下のようにも書けます。(普通は面倒なのでまとめて一行で書いてしまいますが)
List<int> data2 = data1.FindAll(new Predicate<int>(CheckData));
↓
Predicate<int> d = new Predicate<int>(CheckData);
List<int> data2 = data1.FindAll(d);
※ (注意) .NET Framework 2.0 以降では、下記のように、デリゲートを使わずに、直接、関数名をメソッド引数として渡すことができるようなコードを書くことができるようになっています。しかし、これはコンパイラが自動的にコードを補正してくれるためで、内部的にはデリゲートにラッピングされたコードとしてコンパイルが行われます。この機能は実装時には便利なこともあるのですが、今回は簡略化コードを使わずに、きちんとデリゲートでラッピングしたコードを書いていくことにしたいと思います。
1: // .NET Framework 2.0 以降のコンパイラでは、下記のコードでもコンパイルが通るが...
2: List<int> data2 = data1.FindAll(CheckData);
3:
4: // 内部的には、下のようなコードに変換された上で、コンパイルされている
5: List<int> data2 = data1.FindAll(new Predicate<int>(CheckData));
では引き続き、タスクスレッドの作成方法について順番に解説していきます。
[マニュアルスレッドの新規作成]
まず、最も基本となる、マニュアルスレッドの新規作成方法について解説します。マニュアルスレッドを新規で作成するには、System.Threading 名前空間にある、Thread クラスのインスタンスを作成し、これを起動します。基本的な実装方法は、以下の通りです。
- マニュアルスレッド上で動作させたい処理を、引数なし、戻り値なしのメソッドとして作成する。
- このメソッドを ThreadStart デリゲートにラッピングして、Thread オブジェクトのコンストラクタに引渡す。
- スレッドインスタンスの .Start() メソッドをたたくと、新規にマニュアルスレッドが起動し、引き渡しておいたメソッドが起動する。
コードサンプルを以下に示します。
※ (注意) スレッドを起動する前に、t.IsBackground = true; という設定をしていますが、この設定を行うとこのスレッドがバックグラウンドスレッドとしてマークされます。Windows フォームアプリケーションを終了する際に利用する Application.Exit() 命令は、「すべてのメッセージループを停止し、アプリケーションウィンドウを閉じ、メインスレッドの Application.Run() 命令を終了する」というものですが、フォアグラウンドスレッドが残留しているとプロセスが終了しません。このため、マニュアルスレッドは、バックグラウンドスレッド設定をしてから起動することが望ましいと言えます。
なお、この ThreadStart デリゲートは、System.Threading 名前空間の下側に定義されており、引数なし、戻り値が void 型のメソッドをラッピングすることができるデリゲートになっています。よって、この方法では、UI スレッドからマニュアルスレッドへとデータを直接引き渡すことができません。もちろん、上記のコードに示したように、共有変数領域を作成しておき、この領域を使ってデータを引き渡すこともできますが、この方法の場合、UI スレッドとマニュアルスレッドが同時にこのデータを操作する危険性があるため、排他制御が必要になります。(スレッド間での処理競合については、以前のエントリ(こちらとこちら)を参考にしてください。)
この問題を解決するために、.NET Framework 2.0 で導入されたのが、ParameterizedThreadStart デリゲートです。以下に具体的な実装方法を示します。
- マニュアルスレッド上で動作させたい処理を、引数 object 型ひとつ、戻り値なしのメソッドとして作成する。
- このメソッドを、ParameterizedThreadStart デリゲートにラッピングして、Thread オブジェクトのコンストラクタに引き渡す。
- スレッドインスタンスの .Start() メソッドに、object 型のパラメータを一つ渡して叩くと、新規にマニュアルスレッドが起動する。
この方法を利用すれば、明示的にデータ変数を引き渡すことができます。なお、この方法で認められている、UI スレッドからマニュアルスレッドへ引き渡せるパラメータは object 型変数 1 つだけですが、object 型ですのでなんでも渡すことが可能です。(複数のデータ項目を引き渡したい場合には、構造体にまとめたり object[] 配列などにして、これを引き渡せばよい) データを受け取ったメソッド側では、これを元のデータ型にキャストしてから利用してください。
では次に、スレッドプールの使い方について解説します。
[スレッドプールへのワークアイテムの追加]
スレッドプールは、マニュアルスレッドと異なり、自力でスレッドを新規に作成して利用するというものではありません。以前のエントリで解説したように、すでに起動しているスレッドに対して、メソッドを引き渡して処理してもらう、という形になります。
具体的には、以下の作業を行います。
- まず、引数として object 型変数を一つ、戻り値として void 型となるメソッドを用意する。
- これを WaitCallback デリゲートに包んで、プールのキューに追加する。(ThreadPool クラスの QueueUserWorkItem() メソッドを利用する)
実装コードサンプルは以下の通りです。
なお、スレッドプールのワークアイテムキューに追加できるデリゲートは、WaitCallback デリゲートのみになっています。WaitCallback デリゲートは、object 型引数ひとつ、void 型戻り値を持つメソッドしかラッピングできませんので、逆に言うと、スレッドプールによって非同期化できるメソッドのパラメータは object 型一つに限られる、ということになります。複数のパラメータを引き渡したい場合には、
- object 型の配列を使って一個の引数にまとめる。
- 構造体のようなクラスを使って一つの引数にまとめる。
- 後述する非同期デリゲートを使う。
のいずれかの方法を使う必要があります。
[ここまでのまとめ]
さて、ここまでの解説を一度まとめておきます。
- マニュアルスレッドを作成するためには、Thread クラスを作成する。
Thread クラスによりマニュアルスレッド上で起動できるメソッドは、引数なし/戻り値 void 型のメソッドか、引数 object 型ひとつ/戻り値 void 型のメソッドに限定される。 - プールスレッドを利用して処理を行わせるには、ThreadPool クラスを利用する。
ThreadPool クラスの .QueueUserWorkItem メソッドを使って、処理を投入する。投入できるメソッドは、引数 object 型ひとつ/戻り値 void 型のメソッドに限定される。
なお重要な注意点ですが、マニュアルスレッドやプールスレッド上で動作しているメソッドから、UI コントロールを読み書き・操作しては絶対にいけません。当然、マニュアルスレッドやプールスレッド上で長時間処理が終わったら、それをエンドユーザに通知したりする必要はあるのですが、そのためにうかつに button1.Enabled = true; とか label1.Text = “処理が完了しました”; とか MessageBox.Show(“Congulaturations!”); とか書いてはいけません。うっかりこれをやってしまうと、アプリケーションがクラッシュする危険性がありますので、十分に注意してください。
※ (参考) なお、マニュアルスレッドとプールスレッドの使い分けに関しては、Windows フォームアプリケーションの場合にはそれほどシビアになる必要はありません。スレッドプールのスレッドには上限があるため、スレッドプールが枯渇しないよう、長時間処理についてはマニュアルスレッドを使う、というのが一般的なベストプラクティスです。しかし多くの業務アプリケーションでは、そもそもそんなにたくさんの処理を同時に走らせるわけではありません。ASP.NET ランタイムなどでは内部的に大量のプールスレッドが利用されますが、Windows フォームでは、背後で走らせたいタスク処理はせいぜい数個程度でしょうから、プールスレッドを使ったところで枯渇現象を起こすことはないでしょう。このため、Windows フォームアプリケーションの場合には、長時間処理であってもプールスレッド上で動作させてしまうことがしばしばあります。
さて、ここまで解説してきた方法だと、マニュアルスレッドやプールスレッドに引き渡せるデータ変数にかなりの制限があることがわかると思います。もちろん object 型なのでなんでも渡せるといえばその通りなのですが、もうちょっと柔軟に引数として好きなものを渡す方法はないのか....と思うのも人情というものでしょう。実は、以下に解説する非同期デリゲートと呼ばれる機能を利用すると、複数の引数を持つメソッドを、プールスレッド上で処理することができるようになります。これについて解説します。
[非同期デリゲートの利用]
ここまでマニュアルスレッドやプールスレッドの使い方について解説してきましたが、これらに対して引き渡すために利用したデリゲートは、すべて .NET Framework 側で定義されているものであり、我々がデリゲートを定義することはしていませんでした。しかし、デリゲートは我々自身で定義することもできます。例えば、string 型引数と int 型引数を 1 つずつ取り、string 型を返すようなメソッドをラップするデリゲートは、以下のように定義することができます。
1: public delegate string MySampleDelegate(string a, int b);
このデリゲートを使うと、string 型引数と int 型引数を 1 つずつ取り、string 型を返すようなメソッドをラッピングしたオブジェクトを作ることができます。
1: // 以下のようなメソッドを作っておいて...(※ パラメタ名は一致していなくても OK)
2: public string MySampleMethod(string x, int y)
3: {
4: ... (何らかの処理) ...
5: }
6:
7: // このメソッドをラッピングしたデリゲートインスタンスを作る
8: MySampleDelegate del = new MySampleDelegate(MySampleMethod);
9:
さて、このデリゲート(上の例では MySampleDelegate)のインスタンスには、.BeginInvoke() というメソッドが定義されています。この .BeginInvoke() メソッドを叩くと、デリゲートがラップしているメソッドに対する呼び出し要求が、スレッドプールのワークアイテムキューにキューイングされます。そしてその結果、ラップしているメソッドが、プールスレッド上で呼び出されることになります。
1: MySampleDelegate del = new MySampleDelegate(MySampleMethod);
2: del.BeginInvoke("Nobuyuki", 123, null, null); // 後ろ 2 つのパラメータはコールバックに利用するもの(今回は解説しません。)
もう少し具体的な使い方として、Windows フォーム上で、非同期デリゲートを使って、ある長時間処理メソッドをプールスレッド上で動かすための手順を示すと、以下のようになります。
- プールスレッド上で実行したいメソッドを作成する。
このときのメソッドパラメータは任意ですが、戻り値は void としてください。 - 作成したメソッドの引数・戻り値に併せた形でデリゲートを定義する。
デリゲートの名称は、メソッド名 + Delegate とすると良いでしょう。(例: MethodX() に対して、MethodXDelegate とする) このデリゲートはここでしか利用しないので、public 宣言する必要はありません。private 宣言で十分です。 - UI スレッドからスレッドプールのワークアイテムキューにワークアイテムを投入する。
デリゲートインスタンスを作成し、BeginInvoke() 命令を発行します。これにより、当該メソッドへの呼び出しがワークアイテムキューに投入され、プールスレッドにより処理されます。(なお、呼び出しの際には、引数の末尾に null を二つつけてください。これらのパラメータは、コールバック処理や戻り値のハンドリングのために利用されますが、複雑なので今回は解説しません。)
具体的なコード例は以下の通りです。
さて、ここで解説したデリゲートが持っている .BeginInvoke() メソッドと、前回のエントリで解説した Windows フォームのコントロールが持っている .BeginInvoke() メソッドは、名前こそ同じですが全く別物であることに注意してください。
- デリゲートが持っている .BeginInvoke() メソッドは、スレッドプールのワークアイテムキューへの投入である。
- Windows フォームのコントロールが持っている .BeginInvoke() メソッドは、メッセージキューへのメッセージ投入である。
この違いをはっきりさせるため、以下のようなアプリケーション(ボタンを押すとプログレスバーが進んでいき、終了するとメッセージが表示される)を作成してみることにしましょう。
UI の内部設計図 は、以下の通りです。
具体的な実装方法は、以下のようになります。
UI スレッドからの、処理タスクの起動
- まず長時間を要する処理を、LongTask() メソッドとして定義します。ここでは例のため、名前と処理回数最大値を引数として取るようにしておくことにしましょう。
- 次に、LongTask() メソッドに対して、引数や戻り値を合わせたデリゲートを定義します。メソッドのすぐ上に定義しておくと都合がよいでしょう。名前は任意ですが、ここではメソッド名+Delegate という名前をつけることにします。
- ボタンが押されたら、デリゲートを使って、プールスレッド上でこの LongTask() メソッドを動作させるようにします。
プールスレッドからの UI の更新
- ここまで解説してきたように、プールスレッドから直接 UI を更新することはできません。そこで、UI を更新したい処理(プログレスバーへの表示とラベルへの表示処理)を、メソッドとして定義し(UpdateProgressBar(), UpdateLabel() メソッド)、それぞれに対してデリゲートを作っておきます。
- さらにメッセージキューへメッセージを投入するため、コントロールの .BeginInvoke() メソッドを使い、これらの処理を UI スレッド上で動作させます。
このように、UI スレッドとプールスレッドの連携協調動作には、デリゲートや .BeginInvoke() メソッド(2 種類)が利用されることを覚えておいてください。
なお、上記のサンプルでは、プールスレッドから UI スレッドへ処理を移す際に、this.BeginInvoke() メソッドを叩いていますが、この “this” はフォームそのもの(Form1)を示しています。実は、通常の Windows フォームアプリケーションでは、すべてのコントロールが同一の UI スレッドに属しており、そのような場合には、どのコントロールの .BeginInvoke() メソッドを叩いても同じ結果となります。ですので、上記のサンプル中の this.BeginInvoke() メソッドは、button1.BeginInvoke(), label1.BeginInvoke(), progressBar1.BeginInvoke() などと書いても同じ結果となります。
※ 参考(ちょっと難しいので、わからない人は読み飛ばしてください。)
デリゲートが持っている .BeginInvoke() メソッドを利用する場合は、本来のメソッド引数の後ろにさらに 2 つの引数を付与する必要があり、上記のサンプルでは null をつけていました。この 2 つの引数をうまく使うと、戻り値を持つメソッドへの呼び出しを非同期化したり、その結果を取り出したりすることができます。しかし、特殊な理由がなければ、Windows フォームのプログラミングではこの機能は利用する必要はありません。
例えば、非同期デリゲートを利用して、戻り値を持つメソッドへの呼び出しを非同期化する例を考えてみます。この場合には、以下のような設計と実装になります。
上記のコードを見てみると、確かに、後ろ 2 つのパラメータを使うことにより、プールスレッド上で開始した非同期処理の戻り値を受け取るメソッド(これをコールバック関数といいます)を作ることができます。しかし、この処理は UI スレッド上では動作していないため、結局、ここから UI の更新を行うことができません。結果として、上記のような面倒なコーディングが必要になってしまいます。これならいっそ、下に示すコードのように、コールバック関数を使わず、普通にメッセージキューへメッセージを投入するようなプログラムを書いた方が単純です。
このように、デリゲートが持つコールバック機能を利用すると、
- 戻り値を持ったメソッドへの呼び出しを非同期化する(プールスレッド上で動作させる)。
- その戻り値を、別のメソッド(コールバック関数)で受け取る。
ということが可能になるのですが、どちらかというと、コールバック関数を使わずに済ませるプログラミングの方が素直でしょう。(もともとコールバック関数は UI を持たない通常のマルチスレッドプログラミングで使うものなので、UI を持つアプリの場合には、コントロールの .BeginInvoke() だけを使った方が簡単なのですね^^。) なので、この機能については忘れてしまって OK です。
では、最後にちょっとした応用として、タイマの使い方について解説します。
[タイマの利用]
定期的に何らかのタスク処理を行いたい場合、タスクスレッドを起こしてそこでビジーループを作って待機することは、リソース利用上望ましくありません。むしろこのような場合には、.NET Framework 内に用意されているタイマオブジェクトを利用すると便利でしょう。
ただし注意したいのは、.NET Framework の中には 3 種類のタイマがあり、適切な使い分けが必要になる、という点です。具体的には、以下の 3 種類のタイマを使い分けていただく必要があります。(いずれも名称は “Timer” クラスですが、中身や機能は全くといっていいほど異なります。)
- System.Windows.Forms.Timer クラス (Windows アプリ向け)
定期的に Windows メッセージキューにメッセージを投入してくれるもの。 - System.Timers.Timer クラス (汎用タイマ)
定期的にスレッドプールのワークアイテムキューにワークアイテムを投入してくれるもの。 - System.Threading.Timer クラス (低水準タイマ)
低水準 API を提供するタイマ。1. や 2. の内部で使われている。
このうち、3. の System.Threading.Timer クラスは低水準タイマであるため、ほとんど使う必要はありません。基本的には 1. を中心に使い、場合によって 2. を併用する、という形になります。それぞれについて、具体的なコード例を示します。
1. System.Windows.Forms.Timer クラス (Windows アプリ向け)
まず、System.Windows.Forms.Timer クラスは、定期的な UI 更新処理に利用するタイマです。Windows フォームのツールボックス一覧に表示されている部品になりますので、画面に貼り付けて利用します。
利用する際は、timer1_Tick イベントハンドラの記述と、Interval プロパティへのタイマ発生間隔(msec)の設定を行います。このようにすると、System.Windows.Forms.Timer クラスは、定期的にメッセージキューにメッセージを投入してくれます。コード例と、内部動作の概念図を以下に示します。
1: private void timer1_Tick(object sender, System.EventArgs e)
2: {
3: // ここでは長時間処理は絶対にしないこと!(0.1sec ルール)
4: label1.Text = DateTime.Now.ToString(); // BeginInvoke()は不要
5: }
6:
7: private void button1_Click(object sender, EventArgs e)
8: {
9: timer1.Enabled = !timer1.Enabled;
10: }
この System.Windows.Forms.Timer コントロールは、メッセージキューに定期的にメッセージを投入するコントロールです。このため、動作上、以下の特徴や制約があります。
- イベントハンドラ(timer1_Tick)は UI スレッド上で動作するため、直接、UI を更新してよい。(.BeginInvoke() メソッドを使う必要はない。)
- イベントハンドラ(timer1_Tick)は UI スレッド上で動作するため、長時間処理をしてはならない。
よって、この Timer コントロールは、時計の表示などの単純な画面更新タスクの非同期化に利用するのが都合がよいでしょう。
2. System.Timers.Timer クラス (汎用タイマ)
次に、System.Timers.Timer クラスについて解説します。こちらは、定期的な業務処理を行うために利用するタイマで、スレッドプールのワークアイテムキューにワークアイテムを定期的に投入してくれるものです。
先ほどの Windows フォームの System.Windows.Forms.Timer クラスとは異なり、こちらは画面に貼り付けて利用する部品(コントロール)ではなく、通常のオブジェクトになります。実装例を以下に示します。
この System.Timers.Timer クラスのタイマーには、以下のような特徴があります。
- スレッドプールのワークアイテムキューに定期的に処理を投入する。
- UI スレッドをブロックしないため、時間のかかる処理も実施できる。
- 半面、UI 更新のためには BeginInvoke() を利用する必要がある。
特に、最後のポイントについては注意してください。このタイマーのイベントハンドラ(上記のコード例の場合には t_Elapsed() メソッド)は、プールスレッド上で動作しますので、直接、UI コントロールを操作してはいけません。必ず、UI コントロールの BeginInvoke() メソッドにより、UI スレッドへの処理投入を行う必要があります。
(参考&応用) なお、少し裏ワザ的な機能になりますが、System.Timers.Timer クラスの Synchronized プロパティを使うと、t_Elapsed イベントハンドラを UI スレッド上で動作させることができます。しかし、この機能を使うぐらいなら最初から System.Windows.Forms.Timer コントロールを使った方がラクなので、そちらをお勧めします。
3. System.Threading.Timer クラス (低水準タイマ)
上記 2 つのタイマは、それぞれ
- 定期的な UI 更新タスクを動かしたい → System.Windows.Forms.Timer コントロール
- 定期的な業務処理を動かしたい → System.Timers.Timer クラス
というように使い分けますが、これらの内部で低水準 API として利用されているのが、ここで解説するSystem.Threading.Timer クラスになります。ただし、こちらは低水準 API であるため、基本的に使いません。参考までに実装例を以下に示しますが、通常は使わない、ということを覚えておいてください。
以上で解説した 3 種類のタイマの使い分け・比較をすると、以下のようになります。実際に利用するのは、1. と 2. のタイマである、ということを覚えておいていただければと思います。
[今回のエントリのまとめ]
というわけで、ここまで様々なタスクスレッドの起動方法について解説してきましたが、それぞれのタスクスレッドの起動方法には様々なトレードオフがあります。
- 起動パラメータ引渡し可否
- 動作スレッドの種類
- 記述できる処理の長さ
- UI オブジェクト操作時のスレッド同期要否、etc…
これらを比較表としてまとめると、以下のようになります。
もちろん、いずれも一長一短があるわけですが、基本的には以下のように使い分けるとよいでしょう。
- 通常のタスクスレッドの起動には、非同期デリゲートを使う。
非同期デリゲートは最も汎用性が高く、制限が少ないためです。 - 定期的な UI 更新処理については、System.Windows.Forms.Timer コントロールを使う。
ただし、イベントハンドラでは XML Web サービス呼び出しなどの長時間処理は行ってはいけません。 - 定期的な業務処理については、System.Timers.Timer クラスを使う。
ただし、イベントハンドラでは UI を直接操作してはいけません。 - プールの枯渇を考えなければならないような場合は、マニュアルスレッドの利用を検討する。
タスクスレッドの起動については、実装コードを見て「なるほど」と思っても、実際に自分でプログラミングしてみると意外に手詰まりしてしまうことが多いです。今回示したサンプルコードを実際に一度手を動かして組んでみると、なるほどと納得できるところも多いと思いますので、ぜひ一度トライしてみてください。
はじめまして、toronekoと申します。
いつも拝見させて頂いています。
このページに限らずどのエントリも丁寧に時間をかけて作っておられるのが素晴らしいです。
最近ではRSSリーダーの更新通知が待ち遠しいくらいお気に入り(^^;
是非とも頑張って下さい!!
今回はちょっと気になった点が2つ(細かいですけど)
・このエントリのタイトルに「Part 2.」が抜けていませんか?
・最後の表の「マニュアルスレッドの新規作成」で「パラメータの引渡し」が「不可」と
なっていますが、ParameterizedThreadStart の話を考えると2通りありませんか?
です。
ご検討下さい(^^
toroneko さん、コメントありがとうございます。:-) コメントいただけると非常に励みになります^^。更新はかなり気まぐれですが、よかったらまたいらしてください。
> ・最後の表の「マニュアルスレッドの新規作成」で「パラメータの引渡し」が「不可」となっていますが、ParameterizedThreadStart の話を考えると2通りありませんか?
はい、ご指摘の通りです。というか表が .NET 1.0 のまま修正してなかったのでバグりました;。(ParameterizedThreadStart は 2.0 以降の新機能のため)
> ・このエントリのタイトルに「Part 2.」が抜けていませんか?
実は "Part 2." と入れるとなぜかサーバが受け付けてくれなかったので、仕方なく省きました。(タイトルが一意でない、とか言われて怒られるという。) 多分、日本語タイトルを意識してくれないせいだと思うのですが、まあ本質的な話じゃないしいいや、というわけで省きました。
# ……というかこれが分からなくて、アップロードに実に 3 時間近く苦しんだという。(涙)
というわけで、ご指摘ありがとうございました^^。
今後ともよろしくお願いします。:-)
こんにちは、ダッチです。
解説がすごくわかりやすくて勉強になります。
まだ全部の記事を読めてませんが、これからぜひ読んでみたいです!
それで、本題とは直接関係ない質問で申し訳ないのですが、
System.Threading.Timer クラスについて
> こちらは低水準 API であるため、基本的に使いません
と書かれています。
しかし私の中では逆で System.Timers.Timer クラスを基本的に使わないのでは? と思っています。
というのも『プログラミングMicrosoft .NET Framework 第2版』という書籍の 649 ページにそのように記載されているからです。
System.Timers.Timer クラスを使用しない理由を簡単にまとめると、
・System.Timers.Timer クラスは System.Threading.Timer クラスのラッパーである。
・Microsoft がスレッディングとタイマーについてどのようにするか決めている最中に作られたものである。
・みんなが System.Threading.Timer クラスを使用するために System.Timers.Timer クラスは削除されるべきだった。
という内容です。
このように反対の考え方もあるようなのですが、
これについてどう思われますか?
# 今後どちらのタイマーを使用していったらいいのか、わからなくなってきました。
ダッチさん、こんにちは。リプライが半年近くも遅れてしまってすみません。
> しかし私の中では逆で System.Timers.Timer クラスを基本的に使わないのでは? と思っています。
> このように反対の考え方もあるようなのですが、これについてどう思われますか?
ライブラリの使い方に関しては、「どちらの意見が正しいともいえない」と思います。
私の考え方については、p&p の CAG (Client Application Guidance) を元にしているのですが、おそらくこれは開発者の立場によってどちらを是とするのかが変わるのではないかと。
例えば、私の立場からすると、System.Threading.Timer はあまり開発者の人に使ってほしくはないのです。その理由は、System.Timer.Timer クラスはスレッドセーフで使いやすい半面、System.Threading.Timer クラスは、開発者にそれ相応のスレッドなどの知識を求めるからです。(いやまあもっといえば、マルチスレッド処理を書いてほしくないのですけどね^^。バグを出しやすくなるので。)
明確に「今後は使わないでください」という属性がない限りは、どちらを使ってもいいんじゃないでしょうか? というのが私の考えです。
ダッチさんがどちらのライブラリを使うのか、に関しては、ダッチさんが開発されようとしているアプリケーションのタイプによって決めればよいと思います。もし低水準なミドルウェア的なものを作ろうとしているのであれば System.Threading.Timer を使った方がよいでしょうし、高機能な業務アプリケーションを作ろうとしているのであれば、もっと高水準な API (できれば System.Windows.Forms.Timer のようなもの)を使った方がよい、ということになると思います。
> ダッチさん、こんにちは。リプライが半年近くも遅れてしまってすみません。
いえいえ、お忙しいところ回答してくださってありがとうございます。
> ライブラリの使い方に関しては、「どちらの意見が正しいともいえない」と思います。
そうですね。どちらの Timer にも役割を持っていますから、それにあった使い方をすればいいですね。今後は System.Timers.Timer も使ってみます。
.NET Framework の勉強をしていく中で、理解していないものについて、使わないほうがいいよって言う内容を見つけると、理由がわからなくてもとりあえず使わないようにしようって思ってしまいます。もし、書籍よりこちらの記事を最初に読んでいたら System.Threading.Timer を使ってなかったと思います。
使わない理由を理解できれば、使うべき場面もわかってくると思うので、そのためにはもっと深く理解できるように勉強しないとだめですね(^^)。