C#で非同期処理をしよう


はじめに

 このベージでは、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 tasktask.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だけ別に書くかも……

最後に

 初投稿なので、誤字脱字や不足している部分等がございましたらお知らせしていただけると幸いです。