はじめに
このベージでは、C#での非同期処理についてサンプルコードを交えて説明します。
ある程度C#に触れて、「非同期処理をやってみたいな」と思った人向けです。
「こう書けばこのような処理が出来る」程度にしか書いていないので、説明不足が多いかもしれませんがご了承ください。
C#の言語バージョンはC# 7.0以上、.NET Framework 4.7程度です。
非同期処理とは?
非同期処理と言う言葉について調べても理解が出来ない人は多いと思います。(実際、私もそうでした…)
そこで、図を交えて説明します。
非同期処理には大きく分けて2種類あります。並列処理とバックグラウンド処理です。並列処理はその名の通り、複数のスレッドを用いて処理することを言います。バックグラウンド処理は重たい処理や時間がかかる(いつまでかかるかわからない)処理(I/O待ち等)を、別のスレッド(バックグラウンドスレッド)を用いて処理することです。
赤:何らかの処理 緑:UIの更新・変更等 黄色:I/O待ち等
非同期処理の利点
並列処理は、同時に処理を行うことが出来るため、例えばゲームしながら宿題をする、といったようなことが出来ます。それと、スレッド資源の有効活用にもなるので、処理によっては同期処理に比べて速くなります。
バックグラウンド処理は、別のスレッドで時間がかかる処理を行うため、UIを更新しているスレッド(UIスレッド)を止めることがない。つまり「UIをフリーズさせることなく」時間がかかる処理を行うことが出来る。
Threadクラスを用いたタスク並列
using System;
// ThreadクラスはSystem.Threading名前空間に存在
using System.Threading;
namespace Sample {
class C {
static void Main()
{
Thread thread = new Thread(ThreadWork);
thread.Start();
Console.WriteLine("キー入力があるまでスレッドが動き続ける");
Console.ReadKey();
// スレッドを止める(スレッドが止まっている可能性がある場合は
// IsAliveプロパティがtrueか確かめる)
thread.Abort();
}
// スレッドを用いて動かすメソッド
static void ThreadWork()
{
while(true) {
Console.WriteLine("ThreadWork内の処理");
Thread.Sleep(2000);
}
}
}
}
このコードは新たにスレッドを生成して、そのスレッドによって処理を行うものです。子の様に異なるタスクを並列に処理する事をタスク並列と呼びます。
しかし、2行も使って並列処理をするのはめんどくさいですよね? あと、thread.Abort();
をいちいち使ってスレッドを止めないといけないし・・・。
それとThread
クラスはスレッドを生成するため、生成のイベントを通知したり、新しいスレッドの分だけスタックを確保したり・・・とにかく、内部的な面を見てもめんどくさいです。
Taskクラスを用いたタスク並列
そこで、Task
クラスを使用します!
Task
クラスはスレッドプールというスレッドの活用法を使用しています。
スレッドプールは簡単に言うと、スレッドの使い回しです。初期状態では最適に活用できるスレッド数で作成されます。
スレッドを使い回すことによって、スレッド生成による手間(イベント通知、スタック領域の確保等)を省いています。
サンプルコード
using System;
using System.Threading;
// TaskクラスはSystem.Threading.Tasks名前空間に存在
using System.Threading.Tasks;
namespace Sample {
class C {
static void Main()
{
Task task = new Task(ThreadWork);
task.Start();
Console.WriteLine("キー入力があるまでスレッドが動き続ける");
Console.ReadKey();
// Mainメソッドが終了すると自動でTaskが終了する
}
// スレッドを用いて動かすメソッド
static void ThreadWork()
{
while(true) {
Console.WriteLine("ThreadWork内の処理");
Thread.Sleep(2000);
}
}
}
}
これでも十分に動きます。
しかし、普通は「Task
の生成とともに動かしたい!」というのが殆どなので、TaskクラスにはTaskの生成から処理開始までを一行で行えるメソッドが存在します。それがTask.Run
メソッドです。
サンプルコード
using System;
using System.Threading;
// TaskクラスはSystem.Threading.Tasks名前空間に存在
using System.Threading.Tasks;
namespace Sample {
class C {
static void Main()
{
// Task.Run(Action)とTask.Run(Func<Task>)で競合が起こるので、明示的にActionにする
Task.Run((Action)ThreadWork);
Console.WriteLine("キー入力があるまでスレッドが動き続ける");
Console.ReadKey();
}
// スレッドを用いて動かすメソッド
static void ThreadWork()
{
while(true) {
Console.WriteLine("ThreadWork内の処理");
Thread.Sleep(2000);
}
}
}
}
Task task
とtask.Start();
がいらなくなったので、最初のコードと比べると、かなりコード量は減りました。
Task.Run
メソッドはかなり便利です。積極的に使いましょう。
Parallelクラスによるデータ並列
Parallel
クラスはデータが違うが同じような処理を行う時に非常に便利です。
今回はParallel.For
メソッド、Parallel.ForEach
メソッドについてのみ紹介します。
Parallel.Forメソッド
ざっくり説明すると、for
文の処理を並列に行うメソッドです。
サンプルコード
using System;
// ParallelクラスはSystem.Threading.Tasks名前空間に存在
using System.Threading.Tasks;
namespace Sample {
class C {
static void Main()
{
Parallel.For(0, 10, id => {
Console.WriteLine($"並列で動作している部分\t id = {id}");
});
Console.WriteLine("並列処理終了!");
}
}
}
実行結果例
並列で動作している部分 id = 0
並列で動作している部分 id = 2
並列で動作している部分 id = 6
並列で動作している部分 id = 4
並列で動作している部分 id = 8
並列で動作している部分 id = 9
並列で動作している部分 id = 7
並列で動作している部分 id = 1
並列で動作している部分 id = 3
並列で動作している部分 id = 5
並列処理終了!
Parallel.For
メソッドは書き方がfor
文に似ていますよね?(オーバーロードはありますが)for
で書いた直列処理のコードに一手間加える事によって、異なるデータを並列に処理する事(データ並列)が出来ます。
同時に処理をしているため、for
文の様に0番目(id = 0)から順番に出力されるとは限りません。
Parallel.ForEachメソッド
Parallel.ForEach
メソッドはforeach
文を並列に処理するメソッドです。
サンプルコード
using System;
// ParallelクラスはSystem.Threading.Tasks名前空間に存在
using System.Threading.Tasks;
namespace Sample {
class C {
static void Main()
{
int[] arr = { 10, 20, 30, 40, 50, 60, 70, 80, 90 };
Parallel.ForEach(arr, item => {
Console.WriteLine($"並列で動作している部分\t item = {item}");
});
Console.WriteLine("並列処理終了!");
}
}
}
実行結果例
並列で動作している部分 item = 30
並列で動作している部分 item = 40
並列で動作している部分 item = 60
並列で動作している部分 item = 10
並列で動作している部分 item = 50
並列で動作している部分 item = 20
並列で動作している部分 item = 80
並列で動作している部分 item = 70
並列で動作している部分 item = 90
並列処理終了!
Parallel.ForEach
メソッドも書き方がforeach
文に似ていて(オーバーロードはありますが)、foreach
で書いた直列処理のコードに一手間加える事によって、同じくデータ並列が出来ます。
同時に処理をしているため、Parallel.For
と同様に0番目のコレクションから出力されるとは限りません。
async/await
---(書きかけ)---随時追加予定
TPL Dataflow(上級者向け)
---(書きかけ)---随時追加予定
TPLだけ別に書くかも……
最後に
初投稿なので、誤字脱字や不足している部分等がございましたらお知らせしていただけると幸いです。