今回は、C#で非同期プログラミングを作っていきたいと思います。

最近ではあまり見かけませんが、.Netで「スレッド」と検索すると検索結果にThreadクラスを使ったサンプルソースコードに出会うことがあります。しかし、Threadクラスは、.Net初期のバージョンからあるスレッドの記述方法です。.Net4.0以上のバージョンである場合、Taskクラスを使ったほうがオブジェクトの起動速度や今どきのプロセッサを効率よく使用できます。

Taskクラスは、あくまで1つの仕事(アクション)を定義するものにすぎません。しかし、マルチスレッド(非同期)として動作させることができるので、よく考えてプログラミングする必要があります。

C#言語はプログラマーに分かりやすくマルチスレッドを使用してもらうために進化してきたプログラミング言語です。まだまだ進化は続くと思います。
私もまだまだTaskについては学習中ですので、共に勉強出来ればと思います。

今では色々なサイトでTaskのサンプルソースを手に入れることが出来ます。しかし情報量が多く、簡単なサンプルが少なかったりします。そこで、なるべく簡単な説明でまとめてみました。


簡単なTaskクラスの例

C#

private void btn_Job1_Click(object sender, EventArgs e)
{
    Task ts = new Task(this.HeavyJob1);
    ts.Start();
    MessageBox.Show("ジョブ1が開始されました");
    ts.Wait(); // Wait();でメインスレッドを止めることもできる。
}
private void HeavyJob1()
{
    for(int i = 0;  i < 10; i++)
    {
        System.Threading.Thread.Sleep(1000);
    }
}

まず、作成したHeavyJob1メソッドを引数に渡したTaskオブジェクトを作成しています。Task.Startメソッドで引数で渡したアクション(ファンクション)を実行できます。
Task.Waitメソッドでタスクの終了を待ちます。Waitで待ってる間は、UI(ユーザーインターフェース)スレッドは止まるので、画面は固まりフリーズします。

簡単ななTaskクラスを使った例です。


Taskの戻り値を得るには

C#

private void btn_Job2_Click(object sender, EventArgs e)
{
    Task ts =  GetModori1();
    ts.Start();
    //ts.Wait(); // WaitしなくてもResultでUIスレッドは止まってしまう。
    MessageBox.Show(ts.Result.ToString());
}

private Task GetModori1()
{
    // =>を使ってFuncをラムダ式で書いてます。
    return new Task(() =>
    {
        System.Threading.Thread.Sleep(5000); // 重い処理
        return 123;
    });
}

GetModori1メソッドは、Taskクラスを返し、結果(result)をintとしたメソッドです。Taskの引数Actionは、ラダム式()=>を使って書くことが出来ます。
戻り値は、Task.Resultで取得します。

async/awaitを使ってUIスレッドをとめないようにする

C#

private async void btn_Job3_Click(object sender, EventArgs e)
{
    this.btnJob3.Enabled = false; // 非同期対応
    string ret = await Task.Run(()=>GetModori2()); // awaitを使い別スレッド待ち。UIスレッドへ
    Task ts =  GetModori3("thread "); // 戻り値Taskを使った例
    ts.Start();
    string ret2 = await ts;
    this.textBox1.Text=ret + ret2;
    this.btnJob3.Enabled = true;
}

private string GetModori2()
{
    System.Threading.Thread.Sleep(1000); // 重い処理
    return "complete!!";
}
private Task GetModori3(string msg)
{
    return new Task(() =>
    {
        return msg + "hoge";
    });
}

今までの例では、Waitした時点でメインスレッドであるUIスレッドが待ちで止まってしまいます。
Task.Runで実行しているところがポイントでawaitを前に付けてます。このようにawaitを付けると、ボタンクリックメソッドから一旦抜けて、他のイベント受付るようになり、画面の描画処理などが止まらなくなります。そして、GetModori2メソッドが終了すると、先ほどのawaitのポジションから以降の処理が流れます。なお、awaitを使ったメソッドはasyncの装飾詞を付けるルールになっています。
async/awaitが使えるようになってから、UIスレッドへのコールバックが非常に楽になりました。

.Net4.0以前の進捗表示

C#

private void btn_Job4_Click(object sender, EventArgs e)
{
    this.btn_Job4.Enabled = false;
    // .Net4.5じゃないとawaitがないので、.ContinueWithで対応
    this.textBox1.Text = string.Empty;// 進捗初期化
    var ts = Task.Factory.StartNew(this.HeavyJob2); // .net4.5からはRunメソッドでもいきなり実行できる
    ts.ContinueWith(hoge => { 
        textBox1.Text = "Complete!!";
        this.btn_Job4.Enabled = true;
        }
        , TaskScheduler.FromCurrentSynchronizationContext());
    
}

private void HeavyJob2()
{
    for (int i=1; i<=5; i++)
    {
        // 昔からあるInvokeでUIスレッドへ表示(.Net4.5からはIProgressがある)
        this.Invoke(new Action(() => 
            this.textBox1.Text = i.ToString()
        ));
        System.Threading.Thread.Sleep(1000); //重い処理
    }
    
}

※ .Net4.0のサンプルになります。
別スレッドで処理を行っていると、Task内の進捗状況をプログレスバーなどに表示したくなりますよね?
HeavyJob2内では、Form.Invokeメソッドを使い、UIスレッドにコールバックしています。このように昔ながらのデリゲート返しで可能ですが、.Net4.5は後述のProgressクラスが用意されています。

この例では、その他に.Net4.0でTask終了を受けとるためにContinueWithメソッドで戻るようにしています。

Progressを使った進捗報告

C#

private async void btn_Job5_Click_1(object sender, EventArgs e)
{
    this.btn_Job5.Enabled = false;
    // Progressを作成。コールバックさせたいメソッド設定。
    Progress progress = new Progress(OnProgressChanged);
    Task ts = Task.Run(() =>
        {
            HeavyJob3(progress); // Progressを引数に渡す
        });
    
    await ts; // 待機
    this.textBox1.Text = "Complete!!";
    this.btn_Job5.Enabled = true;
}

private void HeavyJob3(IProgress prog)
{
    for (int i=1; i<=5; i++)
    {
        prog.Report(i); // コールバックメソッドへ
        System.Threading.Thread.Sleep(1000); // 重い処理
    }
}

private void OnProgressChanged(int val)
{
    // 進捗を表示
    this.textBox1.Text = val.ToString();
}

IProgressインターフェースを継承したProgressの登場で進捗の報告が楽になりました。
Progressを生成するときに、進捗報告するハンドラーを設定しておきます。別スレッド処理中にProgress.Report呼び出すことにより、UIスレッドでハンドラーが処理されます。

複数スレッドの同期

C#

private void btn_Job6_Click(object sender, EventArgs e)
{
    syncObj = new object();
    m_count = 0;
    var ts1 = new Task(() =>
    {
        for (int i = 0; i < 1000; i++)
        {
            this.CountUp();
            System.Threading.Thread.Sleep(3);
        }
    });
    var ts2 = new Task(() =>
    {
        for (int i = 0; i < 1000; i++)
        {
            this.CountUp();
            System.Threading.Thread.Sleep(1);
        }
    });
    ts1.Start();
    ts2.Start();

    Task.WaitAll(ts1, ts2);
    MessageBox.Show(m_count.ToString());
}

private int m_count;
private object syncObj;
private void CountUp()
{
    lock (syncObj)
    {
        m_count++;
    }
}

複数のスレッドを起動させ同一の資源(リソース)にアクセスしていると、デットロックやデータの不整合が発生します。
ここでは、2つのスレッドから同じ引数に対して1000回カウントアップさせてます。lockを入れて同期をとっています。lockを入れないとカウントが2000にならない時があります。


レスポンスのよいシステムを作るには、非同期プログラムは必要不可欠です。しかし、非同期プログラムは、単一スレッドと違いスレッド同士の同期も考えて設計しなくてはなりません。使えそうな場面でうまく取り入れてみてはいかがでしょうか。

スポンサードリンク