C#でのasync&awaitとTaskの使い方と非同期の考え方をわかりやすく解説#1

当サイトではアフィリエイト広告を利用しています。

C#での非同期プログラミング

C#の非同期プログラミングは.NETFramework4.5以降はTaskとasync&awaitを使ったプログラミングを主流です。非同期プログラミングになじみのない人には非同期処理事態がわからないと思うのでそのあたりをじっくり解説した上で,C#でできる非同期プログラミングの具体的な実装方法を解説して行きます。

非同期ってはじめて聞く人には何のことかよくわからないと思いますが,簡単に言うと,例えばデータの検索画面があったとして,検索を実行してから検索が完了するまでに10秒くらいかかるとします。その10秒の待ち時間の間,次のどちらの処理がいいでしょうか?

(A)まったく画面操作ができなくて,画面が固まっていて,動いているのか止まっているのかも分からない。

(B)画面操作ができたり,プログレスバーがくるくる動作して「処理中です…」って感じで進捗状況を出してくれて,処理中を示してくれる。

間違いなく後者のほうがいいですよね?
この場合(A)が同期プログラミング,(B)が非同期プログラミングと呼びます。

(A)は一度にひとつの処理しかできないプログラミング手法なので,検索処理を行っている間は,画面操作が一切できなくなります。

(B)は時間のかかる処理をバックグラウンドで処理させながら,画面操作はそのままできるような実装方法になります。

ただ,プログラミング手法といっても,(B)だけを行うということではなく,通常は(A)のプログラミングを行いながら,バックグラウンドで処理させたいときのみ(B)のプログラミングを実施するという形になるので,通常の(A)の実装に加えて,(B)のプログラミングをマスターする必要があります。

非同期プログラミングとは

非同期プログラミングがあると何がいいのか?

非同期プログラミングをすることによるメリットは,何といっても,アプリケーションを使用するユーザーの操作性が向上することです。

今のユーザーが,アプリケーションの操作に慣れていますから,ボタン押下などの捜査後に,1秒以上(0.6秒以上?)何の反応もない場合は,画面が固まったのかな?と感じます。インターネットで何かを購入する場合に,「購入」ボタンを押下したのに,数秒間何の反応もなかったら,固まったのかな?何かおかしなことになったのかな?と不安になりますよね?

何かボタンを押したり,処理を行ったときは,「処理しています…」と表示し,プログレスバーなどが進捗を示すようにしないと,非常に使いにくいアプリケーションという評価になってしまいます。

C#が出だしたころは,そういった同期的なプログラミングで,数秒や長いときは数分の待ち時間があってもある程度は許される時代もありましたが,今のようにこれだけパソコンが普及し,スマホなどで,いろんなアプリケーションに触れることの多い現代では,そんんプログラミングをしていたら,レベルの低いプログラマーということにされてしまいます。

非同期プログラミングがないと何が悪いのか?

非同期プログラミングがないと何が悪いかといえば,先に記述したとおり,ユーザーの操作性が下がることです。ただ,非同期プログラミングをしないことで何もいいことがないのかといえばそうでもなく,唯一いいことがあります。それは,プログラミングがシンプルで,不具合の起こり難いプログラミングになります。

これは冗談ではなく非常に大事なことです。
われわれプログラマーは毎日プログラミングコードを記述しますが,その記述したプログラミングコードは,10年20年と製品の中で動作し続けます。何か不具合があるとクレームになり,原因の追究と修正,お客様への謝罪,再発防止策の検討,社内報告会での報告などなど,製品販売後に不具合があると,いろいろ大変なことばかりが起きます。

だから,できるだけ不具合のないプログラミングをして行くには,いかにシンプルでわかりやすいプログラミングをするかというのが,非常に重要になってくるので,非同期プログラミングをせずに,同期プログラミングのみでシンプルに書くというのは,あながち悪い話ではありません。

ただユーザーの操作性を考えたときに,やはり現代では非同期プログラミングを必要なため,むちゃくちゃ多用せずに,必要な部分に絞って非同期プログラミングをすることで,ユーザーの操作性と不具合の少ないプログラミングの両方を兼ね備えることができます。

C#での同期的なプログラミング例

まずはじめに,非同期プログラムの動作がよく理解するために,同期的なプログラミングををするとどんな感じになるのかを説明します。

【画面】

Nomalボタン 時間のかかる処理をする
Countボタン ボタンをクリックすると,1ずつカウントアップした値を表示する

【コード】

/// <summary>
/// 同期的な実装
/// </summary>
public partial class ThreadForm : Form
{
    /// <summary>
    /// カウント
    /// </summary>
    private int _count = 0;

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public ThreadForm()
    {
        InitializeComponent();
    }

    /// <summary>
    /// Countボタンクリック
    /// </summary>
    /// <param name="sender">コントロール</param>
    /// <param name="e">イベント引数</param>
    private void CountButton_Click(object sender, EventArgs e)
    {
        this.CountButton.Text = _count++.ToString();
    }

    /// <summary>
    /// Normalボタンクリック
    /// </summary>
    /// <param name="sender">コントロール</param>
    /// <param name="e">イベント引数</param>
    private void NormalButton_Click(object sender, EventArgs e)
    {
        dataGridView1.DataSource = GetData();
    }

    /// <summary>
    /// データ取得(時間のかかる処理:5秒かかる)
    /// </summary>
    /// <returns></returns>
    private List<DTO> GetData()
    {
        var result = new List<DTO>();
        for (int i = 0; i < 5; i++)
        {
            System.Threading.Thread.Sleep(1000);
            result.Add(new DTO(i.ToString(), DateTime.Now.ToString("HH:mm:ss")));
        }

        return result;
    }
}

【動作】

Countボタンを押下すると,カウントアップされた値が,ボタンのTextに表示されます。
Normalボタンを押すと,1秒ごとにDTOというクラスを生成し,5秒後に処理をおえて,DTOのリストが返却され,データグリッド(dataGridView1)のDataSourceにセットしています。

DTOクラスは単純に,データを保持するためのIDと時間だけのクラスです。

public sealed class DTO
{
    public DTO(string id, string datadate)
    {
        Id = id;
        DataDate = datadate;
    }

    public string Id { get; set; }
    public string DataDate { get; set; }
}

この状態で「Normal」ボタンを押下すると,5秒間はこの画面上で何もできなくなります。Countボタンを押してもカウントアップされず,画面は固まった状態になります。
これではこの画面は処理中なのか,何かおかしなことになって,止まってしまっているのかが判断できなくなり,ユーザーにとっては使い難いアプリケーションということになります。

これらの問題に対応するための対策が,次から解説する非同期プログラミングになります。

C#での非同期プログラミング実装方法

非同期プログラミングといっても,それをC#で実装するには,いくつかのやり方があります。複数ある理由は用途ごとに異なる部分もありますが,.NETFrameworkの時代ごとに非同期プログラミングを簡単に書けるように進化しているから,というのが主な理由です。

最新の.NETFrameworkを使える環境にある場合は,後述するTaskとasycn,awaitを使ったプログラミングをするのがよいですが,.NETFrameworkが4.5より前のものであればasync,awaitは使えませんし,.NETFrameworkが4.0より前のものであればTaskは使えませんので,その場合はThreadPoolを使用したプログラミングをする必要があります。

非同期プログラミングの用語

スレッド

非同期プログラミングの用語として,スレッドというものがあります。スレッドとは,処理をする「道」のようなものという理解でよいと思います。

例えばボタンを押してデータが画面に表示する際に行われる処理は次の3つの処理に分けることができます。

  1. ボタンを押下する
  2. 検索処理をする
  3. データを画面に表示する

この場合,3つの処理が同じ道を通ってしまうと,どこかの処理に時間がかかった場合,その後の処理は前の処理が完了するのを待たされることになります。
今回の例で言えば,処理2「検索処理をする」に5秒かかってしまうことから,処理3「データを画面に表示する」の処理に移れず,その間は画面が固まるという現象が起こっています。図にすると次のようになります。

真ん中の一本の線がスレッドです。これを「処理」が通る「道」です。C#で作成したアプリケーションは,画面起動と同時にUIスレッドというユーザーインタフェース用のスレッドが1つ出来上がります。なので,特に意識せずにプログラミングをしていれば,このUIスレッドという道を処理は通って行きます。そのスレッド上で,大量データの検索などの時間がかかる処理が通ってしまうと,ユーザーインタフェースはその間なのもできなくなってしまいます。そこでこのUIスレッド以外に処理が通れる道を作ってあげることができれば,検索処理を待っている間も,ユーザーインタフェースは動作が可能になります。

UIスレッドとは別にスレッドを生成し,そこに検索処理を通します。こうすることで,検索処理中も,画面は固まらずに処理を行うことができるようになるため,先の例で言えば,検索処理中もCountボタンを押下すれば,カウントアップする処理は動作するようになります。

非同期処理とはこういった具合に,時間のかかる処理などで,ユーザーインタフェースを固めてしまいそうなタイミングで,時間のかかる処理を別のスレッドに通してあげるようにプログラミングして行くことです。ですので,時間のかかる処理を見つけたら,別スレッドにまわして行くということを考えながらプログラミングして行きましょう。

それでは次からは具体的な実装方法に移ります。