vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【C#】Task の話 その 1

はじめに

.NET Framework/.NET Core/Mono だけでなく、 Unity でも Task クラスが使えるようになって久しい今日この頃。

相変わらずぼんやり何となくしか使えないのもなぁ。ということで、ちょっと調べてみることにしましたよ。というお話。

環境

  • Windows 10 ver.1803 17134.523
  • .NET Core ver.2.2.101
  • Rider ver.2018.3.1

Task クラスについて

そもそも Task クラスってどのようなクラスなのでしょうか。

Microsoft Docs を見てみます。

非同期操作を表します

うむ。わかりやすい。

…すみません嘘です/(^o^)\

ま、まぁとにかく非同期処理に関係するクラスであることはわかりました。

非同期処理に関係するクラス

ここで言う非同期処理、とは、ある処理を、他の処理を止めずに、また他の処理によって止められることなく実行できる、というくらいの意味として進めたいと思います。

では、 C# で非同期処理に関係するクラスって Task を含めてどのようなものがあるのでしょうか。

スラスラわかる C#によると、マルチスレッドに関するクラスとして下記の 4 つが挙げられています。

  1. Thread
  2. ThreadPool
  3. Task
  4. Parallel

1.、2.は OS に近い低級なクラス、3.、4.は1.、2.を利用して、より C# で扱いやすくしてくれる高級なクラス、という位置づけのようです。

中でも一番のベースとなっているのは Thread クラスのようです。

また、キーワードとしては async/await が挙げられますね。

後でも触れる通り、マルチスレッドによる処理 = 非同期処理 ではないと思いますので、他にも関連するクラスはあるかもしれません。

登場人物

今回の話のキーワードとなりそうなものを思いつく限り挙げていきます。

プロセス

そもそもプログラムはどのように動くのでしょうか。

という話に大きく関係するのがプロセスです。

インサイドWindows 第7版 上 によると、プログラムを実行するときに必要になる一揃いのコンテナーであり、その中には下記が含まれるとのこと。

  1. プライベート仮想アドレス領域
    • プログラムが利用できるメモリ領域のアドレス
  2. 実行可能プログラム
    • 実際に処理を実行するプログラムで 1.上に展開する形で動作する
  3. 開かれたハンドルのリスト
    • 色んなリソースを識別するためのもの
  4. セキュリティコンテキスト
    • 管理者、一般ユーザーなどの権限、UAC (ユーザーアカウント制御)の設定状態などを識別するためのアクセストーク
  5. 1つのプロセスID
    • そのプロセスを認識するための ID
  6. 一つ以上の実行スレッド

今回の話に特に関連がありそうなのは、1.、2.、6.ですね。

スレッド

インサイド Windows によると、 Windows が実行をスケジュールするプロセス内のエンティティ、とのこと。

うむ。わかりやry

いろいろググってみたところ、

  • CPU (他の装置や回路の制御、演算処理を行う)の利用単位として扱われる
  • プログラムはスレッドを作成し、実行待ちキューに登録する → CPUは登録されたキューを順に実行する -> スレッドが実行されることでプログラムの処理が実行される
  • 1 つの CPU (1 コア) が同時に実行できるスレッドは 1 つ

といったことのようです。まだぼんやりしていますが。

同時に実行できる処理は CPU 数の分だけ?もっとたくさん実行したい、という話ですが、これを解決するためにマルチタスクやマルチスレッドがあります。

マルチタスク

1つのCPUで2つ以上の処理を同時に実行したい場合、それぞれの処理を高速で切り替えながら処理を実行していくことで、瞬間的にそのCPUが実行できる処理は一つだけでありながら、まるで同時実行できているように見せる技術です。

マルチスレッド

1つのプロセス内にあるスレッドに対して、マルチタスクと同様の考え方でスレッドの高速切り替えを行い、同時実行ができているように見せる技術です。

並行プログラミング(Concurrent Programming)

2 つ以上の処理を同時に実行します。

これを実現する方法として、マルチスレッドや次の並列プログラミングがあります。

逆に言えば、処理が同時に実行できれば方法は問わない、と考えることもできます。

並列プログラミング(Parallel Programming)

複数の CPU コアを使って、それぞれ別々に処理を実行します。

PC が 2 つ以上の CPU コアを持っていることが前提となるため、ハードウェア的な制限はありますが、パフォーマンスの向上に役立ちます。

※定義は上記以外も色々ありそうですが、 Concurrency in .NET - Manning 辺りを参考に、このような理解で進めていきたいと思います。
 間違いに気づいたら修正します。

マルチタスクは置いておくとして、他の 3 つ(特に並列プログラミング)は Task クラスを使って実現できるのか、できるとすればどのように書くのか、などを見ていきたいと思います。

Task クラスを使った簡単な操作

ということで、まずは Task を使った簡単そうな処理を書いて動かしてみます。

Program.cs

using System;

namespace TaskSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var player = new TaskSamplePlayer();
            player.Play();
        }
    }
}

TaskSamplePlayer.cs

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TaskSample
{
    public class TaskSamplePlayer
    {
        public void Play()
        {
            Task.Run(() =>
            {
                Console.WriteLine("Hello world!");
            });
        }
    }
}

これを実行すると、なんと、何も出力されませんw

Task クラスが完全に理解できない。。。(´・ω・`)

つまり、 Task.Run の中の処理は非同期で実行されるため、処理が終わる前にメインの処理が終わってしまう、ということになります。

確認のため Program.cs で処理の完了を待つと正しく 「Hello world!」と表示されます。

Program.cs

using System;

namespace TaskSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var player = new TaskSamplePlayer();
            player.Play();
            Thread.Sleep(100);
        }
    }
}

では、実際に Task.Run では何が行われるのでしょうか。

というところを次回辿ってみます。

参照