LoginSignup
121
142

More than 5 years have passed since last update.

【C#】Task初心者のTask初心者によるTask初心者の為のTask入門

Posted at

背景

Taskってなんだか小難しい割に初心者向きの、というかTask入門的な記事が少ない気がする。というか少ないです。

以前書いた、【C#】Cドライブ以下にある全てのファイルパスを非同期かつIEnumerableに取得してみたという記事ではかなり丁寧に書いたつもりだったが「コピペで動くようなものを書け」という要望があったのでこの記事ではコピペで動く、そして分からないが分かるをコンセプトに書いてみました。

問題提起

同期の後輩がこのような事で悩んでいる、という事を聞きました。

WinFormTimerイベントを使わずに1秒毎にファイルの中身を取得したい。
Timerイベントを使いたくない理由としては何故かウィルスソフトに過剰検知されてしまうからで、Taskを使って実現してみたい。だが、イマイチ書き方がわからない…。

おk把握。つまり、こういう事ですね。

Form1.cs
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp6
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        /// <summary>
        /// ロードイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Form1_Load(object sender, EventArgs e)
        {
            Task.Run(async () =>
            {
                var builder = new StringBuilder();
                var csvFileName = @"C:\temp\hoge.csv";

                while (true)
                {
                    byte[] result;
                    builder.Clear();

                    using (var file = new FileStream(csvFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                    {
                        result = new byte[file.Length];

                        await file.ReadAsync(result, 0, (int)file.Length);
                    }

                    foreach (char c in result)
                    {
                        if (char.IsLetterOrDigit(c) || char.IsWhiteSpace(c))
                        {
                            builder.Append(c);
                        }
                    }

                    Invoke(new Action(() =>
                    {
                        textBox1.Text = builder.ToString();
                    }));

                    await Task.Delay(1000);
                }
            });
        }
    }
}

実行画面
非同期7.gif

コメント無しでも分かる方はきちんと理解できていると思います。
解説は最後にして、一旦は根本的な所から見直していきましょう。

なぜTaskを使う必要があるのか

さて、そもそも論、何故Taskが必要なんでしょうか。

この記事を読んでいるという事はふんわりだとしてもなんとなく理解しているはずで、
Taskを使えば画面が固まらなくなる」という認識で大丈夫だと私は思っています。
上記のプログラムも、画面を固めずにファイルの中身を取得したいと言い換えられますしね。

Taskはなぜ難しいのか

色々な要因はあると思うのですが、C#初心者からすれば前提となる知識が圧倒的に足りていないという要因を個人的には推したい。
というのもTaskに関する記事はあっても、そこで記述されている事がなんなのかを説明されていない事が多いです。

自分なりに考えた結果、以下の図がTaskの最低限の構成要素です。下から知識を積み上げていけば、自然とTaskもわかるはず。
image.png

ジェネリクスについて

下の階層から積み上げていきましょう。
List<T>とかでよく見るアレ。なんかわかるようでわからん。そこで例を出してみます。

色んな型に対応できるような、2つの値を比較して大きい値を返すメソッドをつくってね( ˇωˇ )

Form1.cs
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        var result = Compare(1, 2);
    }
    private int Compare(int x, int y)
    {
        return x > y ? x : y;
    }
    private double Compare(double x, double y)
    {
        return x > y ? x : y;
    }
    private float Compare(float x, float y)
    {
        return x > y ? x : y;
    }
    private short Compare(short x, short y)
    {
        return x > y ? x : y;
    }

………
………
………

}

( ˇωˇ )「正気か?」
ジェネリクス使ってやれば、あらゆる型に対応可能。

Form1.cs
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        var result = Compare(1, 2);
    }

    //ジェネリクスを使って比較メソッドを実装
    //比較可能な型を指定した時にのみ このメソッドを呼び出せる
    private T Compare<T>(T x, T y) where T : IComparable
    {
        return x.CompareTo(y) > 0 ? x : y;
    }
}

まあ要は、<T>に対して型はなんでも入れれますよーって認識でよい。

デリゲートについて

プログラムにおいて、命名する事は悪です。
一々処理に名前つけていったら疲れちゃうので匿名でメソッドを処理できるようにしよう!そうしよう!値とかじゃなくて、メソッド自体を持たせられるようにしようぜ!というので生まれたのがDelegate

Form1.cs
public partial class Form1 : Form
{
    // SomeDelegate という名前のデリゲート型を定義
    private delegate void SomeDelegate();
    public Form1()
    {
        InitializeComponent();

        //SomeDelegate型の変数にメソッドを代入
        //型を明示的に宣言する必要がある
        SomeDelegate hoge = WriteDatetimeNow;

        //このように代入したメソッドを呼び出す
        hoge.Invoke();
    }

    private void WriteDatetimeNow()
    {
        Console.WriteLine(DateTime.Now);
    }
}

イベントに対してイベントハンドラを割り付けるということは頻繁にやると思うのですが、delegateはメソッドそのものなので、イベントに対してのイベントハンドラという役割が担えます。なので、基本的には以下のような使い方をします。

Form1.cs
public partial class Form1 : Form
{
    // SomeDelegate という名前のデリゲート型を定義
    public delegate void SomeDelegate();

    //何らかのイベントを定義
    public event SomeDelegate HogeEvent;

    public Form1()
    {
        InitializeComponent();

        //SomeDelegate型の変数にメソッドを代入
        //型を明示的に宣言する必要がある
        SomeDelegate someDelegate = WriteDatetimeNow;

        //イベントにメソッドを割り付ける
        HogeEvent += someDelegate;
    }
    private void WriteDatetimeNow()
    {
        Console.WriteLine(DateTime.Now);
    }
}

あんまりdelegateって見かけないかもね。

定義済みデリゲートについて

こっからがちょっとややこいです。
Taskを色々な記事で見かける中でなんかよくわかんねーけどこう書けばいいんやなー。というのがこれ。

Form1.cs
Task.Run(() =>
{
    //何かしらの処理
});

Task.Runメソッドに、なに渡してんの?
これ、定義済みデリゲートを渡しています。

なんじゃそら?

Delegateの宣言さえめんどくせえ、もっと簡単に書こうぜ!
で考えられたのがAction<T>Func<T, TResult>
こいつらを定義済みデリゲートといいます。
よく使うやつは使いやすいように定義しこうぜってことですね。

とりあえず、Action<T>Func<T, TResult>を渡せばいいようなので愚直にAction<T>を渡してみる。書いてみる。

Form1.cs
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }
    private void Form1_Load(object sender, EventArgs e)
    {
        Task.Run(new Action(WriteDateTimeNow));

        //もしくはこっち
        Task.Run(() => WriteDateTimeNow());
    }
    private void WriteDateTimeNow()
    {
        Console.WriteLine(DateTime.Now);
    }
}

こういう風にTask.Run(new Action(WriteDateTimeNow));
とかTask.Run(() => WriteDateTimeNow());
とか名前のついているメソッドも勿論渡せます。
渡せますが、

あーでも、メソッドに名前つけたくないよー!!!
はい。

Form1.cs
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }
    private void Form1_Load(object sender, EventArgs e)
    {
        Task.Run(() =>
        //{}の中身が名前のないメソッド
        {
            Console.WriteLine(DateTime.Now);
        });
    }
}

もはやdelegate型の変数宣言さえ要らずに匿名メソッドが書けるようになりました( ˇωˇ )
んで、Action<T>Func<T, TResult>って単語はよく聞くけどなんか違いわからんのや…(´・ω・`)
って方、安心してください。

違いは、「戻り値が void か否か」だけです。

戻り値がvoid型ならAction<T>で、
戻り値がTResult型ならFunc<T, TResult>なだけ。

なるほど、よくわからん!な方の為に、

Form1.cs
Task.Run(
//この()は何なのか
() =>
{
    //何かしらの処理
});

この()の中には、本来パラメーターを指定します。
空の括弧は何のパラメーターも送ってないわけです。

つまり、

Form1.cs
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }
    private void Form1_Load(object sender, EventArgs e)
    {
        //パターン1
        //引数は無し
        //戻り値はvoid
        Action Hoge = () => Console.WriteLine(DateTime.Now);
        Task.Run(Hoge);

        //もしくはこう書く
        Task.Run(() => Console.WriteLine(DateTime.Now));

        //パターン2
        //引数はint型を1つ
        //戻り値はvoid
        Action<int> Fuga = x => Console.WriteLine(x);
        Task.Run(() =>
        {
            var t = 1;
            Fuga(t);
        });

        //もしくはこう書く
        Task.Run(() =>
        {
            var t = 1;
            Console.WriteLine(t);
        });
    }
}

戻り値がvoid型でよい場合に使うのがAction<T>で、

Form1.cs
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        //パターン1
        //引数はint型を1つ
        //戻り値はstring型
        Func<int, string> Hoge = (int x) => x.ToString();
        var hoge = Task.Run(() =>
        {
            var x = 1;
            return Hoge(x);
        });

        //もしくはこう書く
        hoge = Task.Run(() =>
        {
            var x = 1;
            return x.ToString();
        });

        //パターン2
        //引数はint型を2つ
        //戻り値はstring型
        Func<int, int, string> Fuga = (x, y) => (x + y).ToString();
        var fuga = Task.Run(() =>
        {
            var t = 1;
            var v = 2;
            return Fuga(t, v);
        });

        //もしくはこう書く
        fuga = Task.Run(() =>
        {
            var t = 1;
            var v = 2;
            return (t + v).ToString();
        });
    }
}

戻り値がvoid型以外の場合に使うのがFunc<T>です。
業務的に使う場合は、名前空間を汚さないように使ったりしますね。

非同期処理とは

非同期処理ってなんなんでしょうね。逆に同期処理ってなんでしょう。
UIスレッドで処理する事を同期処理、
UIスレッド以外 (例えば、スレッドプール)で処理することを私は非同期処理、だと認識しています。
もう少し言うと、コールバック処理をさせていたら非同期処理、ということです。

スレッドプールについて

まず、スレッドプールへの処理のぶっこみ方法はこれ。

Form1.cs
Task.Run(() =>
{
    //何かしらの処理
});

はい、何回も出てきています。これを書けば何かしらの処理はスレッドプールにぶっこまれます( ˘ω˘)
ちょいちょい出てくるスレッドプールって何なんでしょう。

スレッドプールは、既に作られたスレッドを効率よく使いまわす仕組みの事を言っています。
というのも、スレッドを立てる事は非常に時間が掛かるようなのでそこをパフォーマンスよく処理してやる為に考えられたものですね。

なのでこのTask.Runはスレッドを作るんではなくて、既存のスレッドにコールバックをぶちこんでるだけ、という訳です。

で、まあパフォーマンスがよくなったり画面が固まらなくなるのは嬉しいのだけど、UIを更新できるのはUIスレッドだけなんですわ。

1 ~ 10000までをカウントアップしながら出力するプログラムを愚直に書いてみる。

Form1.cs
using System;
using System.Windows.Forms;

namespace WindowsFormsApp6
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        private void Form1_Shown(object sender, EventArgs e)
        {
            //至極単純にこうやればよいのでは?
            for (var i = 1; i <= 10000; ++i)
            {
                textBox1.Text = i.ToString();
            }
        }
    }
}

これ、期待する結果は得られないんです。
何故なら、全てを同期処理で書いてしまっている為。
全部が終わらないと描画がされない、つまり、何回やったって10000としかでない。

じゃあ非同期で書けばよいのでは?

Form1.cs
using System;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp6
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        private void Form1_Shown(object sender, EventArgs e)
        {
            //非同期処理にしてやれば全てうまくいくはず
            Task.Run(() =>
            {
                for (var i = 1; i <= 10000; ++i)
                {
                    textBox1.Text = i.ToString();
                }
            });
        }
    }
}

image.png
残念、怒られちゃう。
なんでか。だってUIを更新できるのはUIスレッドだけだから。
つまり、どうかけばいいのさ?
結構簡単、Invokeメソッド内でUIの更新を行ってやればよいだけ。

Form1.cs
using System;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp6
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        private void Form1_Shown(object sender, EventArgs e)
        {
            Task.Run(() =>
            {
                for (var i = 1; i <= 10000; ++i)
                {
                    //Invokeメソッド内ではUIスレッドに戻してくれる
                    Invoke(new Action(() =>
                    {
                        textBox1.Text = i.ToString();
                    }));
                }
            });
        }
    }
}

非同期8.gif
諸々の処理はスレッドプールなど、他のスレッドに任せてよいのだけど、UIへの更新はUIスレッドからしか出来ない事は覚えておいた方が捗ります。

Taskについて

スレッドセーフについてはまああんまし関係ないというか、無い事もないですがどちらかというと並列処理に絡んでくるので今回はスルーします。

漸く本題、Taskについてさらっといくよ、さらっと。
問1. 以下のコードを実行した場合、出力結果はどうなるでしょーか。

Form1.cs
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private async void Form1_Load(object sender, EventArgs e)
    {
        Console.WriteLine("A");
        Task.Run(() =>
        {
            Console.WriteLine("B");
        });
        Console.WriteLine("C");
    }
}

答えは、

A
C
B

です。

ほげ?
問2. んじゃこれなら?

Form1.cs
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private async void Form1_Load(object sender, EventArgs e)
    {
        Console.WriteLine("A");
        await Task.Run(() =>
        {
            Console.WriteLine("B");
        });
        Console.WriteLine("C");
    }
}

答えは、

A
B
C

です( ˇωˇ )

問1ではAを出力した後にTaskを作る(つくるだけ)、次にCを出力してTaskが実行される。
問2ではAを出力した後にTaskを作ったうえで待つ、次にCを出力して終了。

ここらへんごちゃってる人多いと思うけどまとめるよ。
awaitキーワードを付けたらその処理が終わるまで待ってくれる。
awaitキーワードを使用するのであれば、asyncキーワードをメソッドの戻り値の前につけてやる
awaitキーワードで待てる処理はTask/Task<T>

問題提起の解説

Form1.cs
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp6
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        /// <summary>
        /// ロードイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Form1_Load(object sender, EventArgs e)
        {
            //巨大なファイルを読み込むつもりならここはあえてawaitキーワードを外すべき
            //UIスレッドで処理をさせると固まってしまうのでスレッドプールに処理を投げる
            //Task.Runメソッド内でawait を使用しているので、asyncキーワードを付ける
            Task.Run(async () =>
            {
                var builder = new StringBuilder();
                var csvFileName = @"C:\temp\hoge.csv";

                //無限ループ
                while (true)
                {
                    byte[] result;
                    builder.Clear();

                    //ファイルオープン
                    using (var file = new FileStream(csvFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                    {
                        result = new byte[file.Length];

                        //バイナリデータを非同期的に読み込む
                        //全部を読み込み終わるまで待っとくで~
                        await file.ReadAsync(result, 0, (int)file.Length);
                    }

                    foreach (char c in result)
                    {
                        if (char.IsLetterOrDigit(c) || char.IsWhiteSpace(c))
                        {
                            builder.Append(c);
                        }
                    }

                    //UIを更新できるのはUIスレッドだけなので Invoke でUIスレッドに一旦処理を戻してやる
                    Invoke(new Action(() =>
                    {
                        textBox1.Text = builder.ToString();
                    }));

                    //タイマーイベントの代わり
                    //1秒待つ
                    await Task.Delay(1000);
                }
            });
        }
    }
}

このページを開いた時には分からなかったとしても、ここまで読み進めてきたらなんとなく読めるようになったのではないのでしょうか。
ちなみに、async/awaitキーワードが使えるのは.NET Framework4.5以上をターゲットフレームワークに指定しているからです。

まとめ

色々ごちゃごちゃ書いたんですが、Taskを理解する為には多くの前提知識を必要としているなとは常々感じていました。じゃあ何が分かっていればTaskの理解へと繋がるのか?ということを振り返ってみて、それを形容化してみました。

ダダダダと書いたので分かりにくい点などあれば、是非ご指摘をお願いします。

121
142
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
121
142