使っているパソコンがマルチコアであっても、プログラムで特に考慮しなければ、コアは1つしか割り当てられません。
並列に処理をさせてあげればプログラムを速く動かすことができるので、プログラムには多くのコアを割り当てたいところです。
そんな時に役に立つのが今回紹介するスレッドです。
ただ、スレッドについては多くのサイトで解説されていますが、イマイチ専門的で分かり難かったりしませんか?
特に掲載されているサンプルは実用的なものが少なく、実際のプログラムでどう書けばよいのか分かりずらいものも多いようです。
今回は「自分のプログラムに応用するには」という視点で、短いサンプルと図を使って、より具体的かつ実践的に解説したいと思います。
マルチスレッドの実現方法
スレッドについてはこちらの記事に記載していますので、ここではマルチスレッドの実現方法について説明します。
冒頭にも申しましたが、普通にVisualStudioでプログラムを作成した場合、そのプロセスはシングルスレッドになってしまいます。
複数の処理を個別のスレッドで並列実行(非同期処理)する、いわゆるマルチスレッド化するには Task というクラスを使います。
Taskを使うには、あらかじめ using による参照設定が必要となります。
1 |
using System.Threading.Tasks; |
戻り値、引数が無い処理のスレッド化
Taskの使い方は簡単で、次の様に Runメソッドの引数に実行したいメソッド名を記述するだけです。
1 2 |
// SubTask1というメソッドを別スレッドで走らせる Task.Run(SubTask1); |
では、具体的なコードを見ていきましょう。
TaskSampleというメソッドの中で、SubTask1とMainTask というメソッドを呼び出しています。
このプログラムを実行すると、SubTask1とMainTask が並列で動作します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
private void TaskSample() { Task.Run(SubTask1); MainTask(); } /// <summary> /// メイン処理 /// </summary> private void MainTask() { Console.WriteLine("メイン処理の開始"); Thread.Sleep(2000); Console.WriteLine("メイン処理の終了"); } /// <summary> /// サブ処理 /// </summary> private void SubTask1() { Console.WriteLine("サブ処理1の開始"); Thread.Sleep(3000); Console.WriteLine("サブ処理1の終了"); } |
実行結果は次の様になります。
1 2 3 4 |
メイン処理の開始 サブ処理1の開始 メイン処理の終了 サブ処理1の終了 |
他のサイトに掲載されているサンプルは、Task.Runの引数にラムダ式を使っているものが多いようですが、その場合は次の様になります。
1 2 3 4 5 6 |
Task.Run(() => { Console.WriteLine("サブ処理1の開始"); Thread.Sleep(3000); Console.WriteLine("サブ処理1の終了"); }); |
戻り値が有る処理のスレッド化
値を返すメソッドを用意して、Runメソッドを呼び出すと、そのプロセス終了後に戻り値を受け取ることが出来ます。
具体的には、Runメソッドの戻り値を取得して、Resultプロパティを参照するだけです。
戻り値は Task<戻り値の型> で定義します。
次のサンプルは1234という整数(int)を固定で返す SubTask1 を、別スレッドで実行するサンプルです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
private void TaskSample() { Task<int> task = Task.Run(SubTask1); MainTask(); Console.WriteLine("戻り値:{0}",task.Result); } /// <summary> /// メイン処理 /// </summary> private void MainTask() { Console.WriteLine("メイン処理の開始"); Thread.Sleep(2000); Console.WriteLine("メイン処理の終了"); } /// <summary> /// サブ処理 /// </summary> private int SubTask1() { Console.WriteLine("サブ処理1の開始"); Thread.Sleep(3000); Console.WriteLine("サブ処理1の終了"); return 1234; } |
実行した結果は次の様になります。
1 2 3 4 5 |
メイン処理の開始 サブ処理2の開始 メイン処理の終了 サブ処理2の終了 戻り値:12345 |
ポイントは戻り値を参照する task.Result の実行場所です。
Resultプロパティを参照するには、当然そのスレッドが終了していなければなりません。
つまり、Resultプロパティを参照した時点で、まだそのスレッドが終了していなければ、終了するまで待機してしまいます。
もしも以下のように、MainTaskの前にtask.Result を実行してしまうと、SubTask1が終了してからMainTaskが開始されるので、並列処理になりませんので、ご注意ください。
1 2 3 4 5 6 7 8 9 10 11 12 |
private void TaskSample() { Task<int> task = Task.Run(SubTask1); Console.WriteLine("戻り値:{0}",task.Result); //<--SubTask1が終了するまで、ここで待機してしまう MainTask(); } |
引数が有る処理のスレッド化
実は、Runメソッドの引数に渡せるメソッドには制限が有ります。
引数が指定できるメソッドが渡せないのです。
引数を指定できるメソッドを呼び出すにはラムダ式を使う必要があります。
理屈は置いておくとして、具体的には次の様に記述することで、引数付きのメソッドを別スレッドで実行することが出来ます。
1 |
Task.Run(() => SubTask2("aaa")) |
ラムダ式でメソッドを呼び出しましたが、処理するコードが少なければ、次の様に全ての処理をラムダ式に記述する事も出来ます。
この場合は message という変数を用意しておいて、ラムダ式の中でその変数を使うという考え方になります。
1 2 3 4 5 6 7 8 9 10 |
string message = "aaa"; Task.Run(() => { Console.WriteLine(message + "サブ処理の開始"); Thread.Sleep(3000); Console.WriteLine(message + "サブ処理の終了"); return message; }); |
複数のスレッドを同期させる
さて、今まではスレッドを並列で処理する(非同期化)方法を紹介してきましたが、場合によっては並列化した複数スレッドのどれか、あるいは全てが終了してから次の処理に進みたいという事も有ります。
いわるゆスレッドの同期化です。
任意のスレッドの終了を待つ
まず最初に、任意のスレッドの終了を待ってから、次の処理に移る方法です。
この場合、Waitメソッドを呼ぶ方法、前述のResultプロパティを参照する方法、await をTask.Runの前に付ける方法の3通りが有ります。
Waitメソッドを呼ぶ
Runメソッドの戻り値を受け取り、そのオブジェクトのWaitメソッドを呼び出す方法です。
1 2 3 4 |
Task task = Task.Run(SubTask1); task.Wait(); MainTask(); |
この処理の実行結果は次の様になります。
1 2 3 4 |
サブ処理1の開始 サブ処理1の終了 メイン処理の開始 メイン処理の終了 |
Resultプロパティを参照する
既に説明済みですが、Resultプロパティを参照する方法もあります。
ただし、この方法は戻り値がある場合のみです。
そもそも、このReulstプロパティはスレッドの戻り値が無いと生成されませんので、ビルドエラーになってしまいます。
もし戻り値が無いのであれば、前述のWaitメソッドか、後述の await を使ってください。
1 2 3 4 |
Task<int> task = Task.Run(SubTask); var val = task.Result; MainTask(); |
awaitを使う方法
await を使う事で、そのスレッドが終了するまで待機させることが可能です。
これはスレッドに戻り値があろうがなかろうが、終了するまで待機してくれます。
1 2 |
await Task.Run(SubTask1); MainTask(); |
ただし、これも注意点があって、await を記述した場合、それを含んでいるメソッドの先頭に async を記述しないとビルドエラーになってしまいます。
例えば、次の様になります。
1 2 3 4 5 |
private async void TaskSample() { await Task.Run(SubTask1); MainTask(); } |
これは仕様なので受け入れるしかありません。
複数スレッドの終了を待つ
複数のスレッドを配列に格納しておいて、Task.WaitAllメソッドに渡してあげると、それら全てのスレッドが終了するまで待機させることができます。
実際のコードは次の様になります。
1 2 3 4 5 6 |
Task.Run(SubTask1); Task.Run(SubTask2); Task[] tasks = new Task[] { task1, task2 }; Task.WaitAll(tasks); |
同時に実行するスレッドの数が少ない場合はこの方法でも良いのですが、実はもっと便利は方法が備わっています。
スレッドをまとめて実行してくれる便利な機能
今までは個々のスレッドを並列に走らせる方法でしたが、例えば10個とか100個とか1000個とか、「似た様な処理をまとめて並列処理させ、全ての処理結果が揃ったら次に移りたい」という場合もあります。
こんな時便利なものとしてAsParallelクラスやLinkのAsParallelがあります。
Parallel.For
Parallec.Forは、forループ内でTask.Runを実行して、最後に WaitAll をするようなイメージです。
Parallel.For(開始値, 終了値, i => 処理));
開始値、終了値はforループの開始、終了条件と同じ考え方になります。
Parallel.For(0,5,i=>~) は、 for(int i = 0;n < 5;i ++) と同じです。
1 |
Parallel.For(0, 3, i => SubTask(i)); |
Parallel.For にはスレッドの上限を決めるオプションがあります。
3つ目の引数に new ParallelOptions(){MaxDegreeOfParallelism = 2} とすると、スレッドの同時実行数が2個に制限されます。
1 2 |
Parallel.For(0,3,new ParallelOptions(){ MaxDegreeOfParallelism = 3 }, i => SubTask(i)); |
Parallel.Foreach
Parallec.Foreachは、foreachループ内でTask.Runを実行して、最後に WaitAll をするようなイメージです。
Parallel.For(配列系の変数, i => 処理));
配列系の変数とは 、正確には「 IEnuerable インターフェースが実装されている型から作られた変数」になるのですが、一般的にいう配列とかListがそれに相当します。
要するにLinqが使える変数です。
foreach(string item in items) と記述する代わりに、 Parallel.ForEach(item,i=>~) と記述します。
1 2 3 |
int[] items = new int[] { 1, 22, 333 }; Parallel.ForEach(items,i => SubTask(i)) |
Parallel.For と同様に、Parallel.ForEach も同時実行するスレッドの上限を指定することができます。
1 |
Parallel.ForEach(items,new ParallelOptions() { MaxDegreeOfParallelism = 2 },i => SubTask(i)); |
AsParallel().ForAll
AsParallel().ForAll を使う事で、Parallel.ForEach と同じことを Linq を使っても実現できます。
1 2 3 |
int[] items = new int[] { 1, 22, 333 }; items.AsParallel().ForAll(i => SubTask(i)); |
Enumerable.Range を使うと、Parallel.For と同じことが実現できます。
1 |
Enumerable.Range(0, 3).AsParallel().ForAll(i => SubTask(i)); |
Parallel.Invoke
ここまでに登場してきたのは、ループ処理の中でスレッドを次々の実行していく方法でした。
ループではなく、列記したスレッドを順に実行していき、全てのスレッドが終了するまで待機してくれるのが Parallel.Invokeです。
これは、まさに Task.Run と WaitAll の組み合わせを簡単に実現する方法です。
具体的なサンプルコードは次の様になります。
1 2 3 4 5 |
Parallel.Invoke( SubTask1, SubTask2, SubTask3 ); |
非同期メソッドとは
非同期メソッドとは、処理の中に非同期処理を含んでいるメソッドのことです。
そして、単にそれを実行するだけで、自動的に別スレッドが立ち上がり、そこで処理が実行されます。
次が非同期メソッドのサンプルソースです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
private void TaskSample() { AsyncTask1(); AsyncTask2(); MainTask(); } //非同期メソッド1 private async Task AsyncTask1() { await Task.Run(() => { Console.WriteLine("サブ処理1の開始"); Thread.Sleep(3000); Console.WriteLine("サブ処理1の終了"); }); } //非同期メソッド2 private async Task AsyncTask2() { await Task.Run(() => { Console.WriteLine("サブ処理2の開始"); Thread.Sleep(1000); Console.WriteLine("サブ処理2の終了"); }); } |
TaskSampleメソッドを実行すると、次のようになります。
1 2 3 4 5 6 |
サブ処理1の開始 サブ処理2の開始 メイン処理の開始 サブ処理2の終了 メイン処理の終了 サブ処理1の終了 |
非同期メソッドは、内部に非同期な処理が含まれているため、ただ単に実行するだけで並列処理が実現できます。
非同期メソッドの書き方
非同期メソッドの中には、必ず1つ以上の非同期処理(Task.Runか、それに代わるもの)が必要です。
一番簡単な非同期処理として、一定時間を待つ Task.Delay() が思い浮かびますが、Http通信を行うための HttpClientクラスに実装されているGetStringAsync メソッドであるとか、ファイルを読み込む StreadReaderクラスのReadLineAsyncメソッドであるとか、メソッドの末尾に Async が付いている多くのメソッドが存在します。
これらの非同期処理を最低1つ含んでいる必要があります。
また、メソッドの中の1つには、必ず await という修飾子が必要で、メソッドの名前にも async が付いていなければなりません。
面白いのは、return が無くても戻り値が自動的に Task になるところです。
非同期メソッドに戻り値が必要な場合は、Task<型>という風に戻り値の型を宣言しておきます。
既存の非同期処理を使わない場合は、 Task.Run で非同期処理を入れておくことでも非同期メソッドが作れます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//戻り値の無い非同期メソッド の例 private async Task AsyncTask1() { await Task.Run(() => { Console.WriteLine("サブ処理1の開始"); Thread.Sleep(3000); Console.WriteLine("サブ処理1の終了"); }); } //戻り値として文字列を返す非同期メソッドの例 private async Task<string> AsyncTask2() { await Task.Run(() => { Console.WriteLine("サブ処理2の開始"); Thread.Sleep(3000); Console.WriteLine("サブ処理2の終了"); }); reutrn "abcde"; } |
非同期処理の前には、必ず await が必要なのですが、これだと非同期処理の終了を待つので、結局非同期処理にならないのではないかという疑問が生じるかもしれません。
しかし、非同期メソッド単位で並列処理させるのであって、非同期メソッド1つ1つにおいては、その中の処理は同期させる必要があるため、await が必要になってきます。
非同期メソッドからの戻り値の受け取り方
非同期メソッドから値を受け取るには Task.Result としたいところですが、これをするとフリーズしてしまいます。
先ほどのサンプルに記載したAsyncTask2() は戻り値として "abcde" を返すようになっていましたが、この非同期メソッドから値を受け取るには次の様に記述します。
1 2 |
string t2 = await AsyncTask2(); |
await を非同期メソッドの前に書いて、普通に string の変数で値を受け取ります。
非同期メソッドから値を受け取りたいので、処理の終了を待つ必要が生じる訳ですが、このため await が必要になります。
別の見方をすると、await を書いているため、この行以降の処理は、AsyncTask2が終了するまで待たされるという事になります。
非同期メソッドから値を受け取るケースの方が多いと思いますが、「値を受け取る=そこで待機する」ということは意識しておいた方がいいでしょう。
それから、値を受け取るために await を書いた場合、それを含んでいるメソッド名には、async を忘れずに記述しましょう。
非同期メソッドでVisual Studioから警告を受けた時の対処
普通に非同期メソッドを記述すると、Visual Studio から警告が出ます。
非同期メソッドから何らかの戻り値を受け取る場合は警告が出ないのですが、戻り値が無い場合は受け取る値が無いので困ります。
ダミー変数で受けようとしても、それはそれで「値○○への不必要な代入」という警告が出ます。
こんな場合は、アンダスコアに代入することで警告を消すことが出来ます。
こういう仕様のようなので、警告が気になる方はこの方法をお使いください。
これはこれで、ちょっと気持ち悪い気もしますが・・・
まとめ
今回はスレッドを使った並列処理について解説しました。
Visual Studio で普通にプログラムを書いて実行しても、スレッドは1つしか使われません。
複数のスレッドで並列処理をさせるには、次に示す方法を使う必要があります。
- Task.Run を使う
- Parallel.Invokeを使う
- Parallel.Forを使う
- Parallel.ForEachを使う
- AsParalle.ForAll を使う
メソッドの内部に await 修飾子付きの非同期処理を含み、メソッド名の前に async という修飾子を持つメソッドを「非同期メソッド」と呼びます。
非同期メソッドは、単純にそれを呼び出すだけでTask.Runをせずともスレッドによる同時実行をしてくれます。
非同期メソッドの中に記述できる非同期処理(メソッド)には多くの種類がありますが、Task.Runで任意の非同期処理を記述することも可能です。
以上のことが要約になります。
スレッドは少しややこしいかもしれませんが、使いこなすとCPUのコアを有効に使って処理時間を短くできますので、機会があれば一度お試しください。