非同期処理【C#】

ここではC#における非同期処理について説明していきます。C#では async / await を使って非常に簡単に非同期処理を実装することが可能になっているので、非同期処理をしっかり理解して活用してみましょう。

非同期処理とは?

与えられた処理を順番に、前の処理が終わったら次に処理に移っていくことで実行していくという従来からある処理の進め方を、それぞれの処理を同期させて進めていくということで同期処理と呼びます。しかし、それでは何か時間のかかる処理を行うときには、他のすべての処理の進行が止まってしまうことを意味します(GUIアプリではこの状態が「フリーズ」に相当します)。それに対して時間のかかる処理を実行している間に何かほかの処理を行う(その2つの処理は同期していない)ことを非同期処理と呼びます。

非同期処理については以下のドキュメントもご覧ください。

async / await を用いた非同期処理の実装

C# 5.0 以降ではasync / await キーワードが実装され、通常の同期処理と同じようなコードで非同期処理が実装できるようになりました。

async修飾子非同期メソッドであることを表し、メソッド内でawait演算子を使用することが可能になります
await演算子そのタスクがまだ完了していない場合は、それが完了するまで以降の処理を中断して待機します
(処理を待機している間は制御を呼び出し元に戻しているので、他の処理を行うことはできます)

それではこれらのキーワードの使い方を見ていきましょう。

async修飾子とawait演算子

await演算子は「待機可能なオブジェクト」に対して、その処理が完了するまで以降の処理を中断させて待機させ、処理が終了したらその処理の戻り値を返します。

基本書式
await [待機可能な式];
コード例①
await task;    //taskはTask型のインスタンス

なお、「待機可能な式」は.NETクラスライブラリであらかじめ定義されていて、Task型、Task<TResult>型、ValueTask型、ValueTask<TResult>型が相当するので、実際にコーディングする際にはコード例のようにそれらを使うだけでよく「待機可能な式」について深く考える必要はありません。

例えばWeb上の指定されたURIから情報をダウンロードするHttpClient.GetAsyncメソッドを考えてみましょう。このメソッドは戻り値がTask<HttpResponseMessage>であり、これに対してawait演算子を使うことが可能です。

HttpClient httpClient = new HttpClient();
Task<HttpResponseMessage> httpTask = httpClient.GetAsync("http://www.microsoft.com/");

// ダウンロード中に他の処理を実行することも可能です

HttpResponseMessage responseMessage = await httpTask;

2行目でGetAsyncメソッドを実行してTask<HttpResponseMessage>インスタンスを取得しています。この時点でメインスレッドとは別のスレッドでダウンロードが開始されており、Task<HttpResponseMessage>インスタンスによってその進捗状況などが管理されています。この時点ではメインスレッドは使われていないので、4行目に他の処理を挿入することも可能です。そして、6行目でTask<HttpResponseMessage>インスタンスに対してawait演算子を使うことで、以降の処理を中断してダウンロードが完了するまで待機します。そして、GetAsyncメソッドの本来の戻り値であるHttpResponseMessage型を返します。

ダウンロード中に他の処理を実行する必要がなければ次のようにまとめてしまうことも可能であり、実際にはこのような表現を目にする機会の方が多いかもしれません。

HttpClient httpClient = new HttpClient();
HttpResponseMessage responseMessage = await httpClient.GetAsync("http://www.microsoft.com/");
コード例②
await MethodAsync();    //MethodAsyncはTask型の戻り値を返すメソッド

なお、async修飾子は「そのメソッドでawait演算子が使われている」ということを示すだけであり、それ自体で何か処理をするわけではありません。

また、非同期メソッドではメソッド名を「〇〇Async」というように最後に「Async」を付け加えるという慣用的なルールがあります。

await演算子の挙動を確かめてみる

それでは、await演算子の実際の挙動を確かめてみましょう。まずは以下のようなDelayAsyncメソッドを作ってみます。

private async Task DelayAsync(int x)
{
    await Task.Delay(1000 * x);
    Console.WriteLine("Delay: " + x + " s");
}

引数xで与えられた秒数だけDelayメソッドで処理を遅延させて、「Delay: 〇 s」のようなメッセージを表示させます。この際、Delayメソッドで処理を遅延させています。このDelayメソッドは別スレッドで実行される代わりに戻り値としてTask型を返すので、その進捗状況が管理されます。ここではawait演算子によってそれが完了するまで以降の処理を中断して待機します。

await演算子を使わずにDelayAsyncメソッドを実行する

まずはawait演算子を使わずに先ほどのDelayAsyncメソッドを実行してみましょう。

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        Program program = new Program();
        Console.WriteLine("処理を開始します");

        program.DelayAsync(2);
        program.DelayAsync(2);

        Console.WriteLine("<ここで他の処理を実行する>");
        Console.WriteLine("処理が終了しました");
    }

    private async Task DelayAsync(int x)
    {
        await Task.Delay(1000 * x);
        Console.WriteLine("Delay: " + x + " s");
    }
}

11行目、12行目でDelayAsyncメソッドに遅延秒数として2秒を指定して実行しています。

DelayAsyncメソッド内のDelayメソッドは別スレッドで実行されていますが、DelayAsyncメソッドを呼び出す際にawait演算子をつけていないので、メインスレッドではDelayAsyncメソッドの処理が完了を待たずに次の処理に進んでしまっています。

つまりawait演算子がないと時間のかかる処理を行う場合もプログラムが待機されることはなく、その終了を待たずにプログラムが終了してしまうことが分かります。この例でもコンソール上に「Delay: 2 s」というのが表示されることもなく、当然2秒間の遅延もなく一瞬でプログラムを終了してしまいます。

await演算子をつけてDelayAsyncメソッドを実行する

続いて、await演算子をつけるとどのように挙動が変わるのかを見てみましょう。

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        Program program = new Program();
        Console.WriteLine("処理を開始します");

        await program.DelayAsync(2);
        await program.DelayAsync(2);

        Console.WriteLine("<ここで他の処理を実行する>");
        Console.WriteLine("処理が終了しました");
    }

    private async Task DelayAsync(int x)
    {
        await Task.Delay(1000 * x);
        Console.WriteLine("Delay: " + x + " s");
    }
}
DelayAsyncメソッドの待機が2回行われた後に14行目が実行されているので、「Delay: 2 s」のあとに「<ここで他の処理を実行する>」が表示されています。

先ほどと同様に、11行目、12行目でDelayAsyncメソッドに遅延秒数として2秒を指定して実行していますが、今度はawait演算子をつけています。

await演算子によってその処理が完了するまで以降の処理を中断して待機してくれるので、まず11行目のDelayAsyncメソッドが終了するまで2秒間メインスレッドが待機されて、最後に「Delay: 2 s」と表示されます。続いて、12行目のDelayAsyncメソッドが終了するまで再び2秒間メインスレッドが待機されて、「Delay: 2 s」と表示されます。

つまり、await演算子によって時間のかかる処理を行う場合はプログラムが待機されたことが分かります。この場合はDelayAsyncメソッドによって2秒間待機されるのが2回あるので、プログラムの終了までおよそ4秒かかります。

DelayAsyncメソッドを実行している最中に他の処理を行う

しかし、先ほどのように単に時間のかかる処理にawait演算子をつけるだけでは上から順番に同期的に処理を行っているに過ぎません。ではこの処理を非同期的に行って、時間のかかる処理を行っている間に他の処理を行う方法を見てみましょう。

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        Program program = new Program();
        Console.WriteLine("処理を開始します");

        Task task1 = program.DelayAsync(2);
        Task task2 = program.DelayAsync(2);

        Console.WriteLine("<ここで他の処理を実行する>");

        await task1;
        await task2;
        
        Console.WriteLine("処理が終了しました");
    }

    private async Task DelayAsync(int x)
    {
        await Task.Delay(1000 * x);
        Console.WriteLine("Delay: " + x + " s");
    }
}
DelayAsyncメソッドを実行している最中に14行目を行っているので、「<ここで他の処理を実行する>」が「Delay: 2 s」よりも先に表示されています。

11行目と12行目でDelayAsyncメソッドを遅延秒数2秒で実行していますが、ここではawait演算子をつけずにその戻り値をTask型として取得しています。これによりそれぞれ別スレッドでDelayAsyncメソッドを開始することができ、その進捗状況はここで取得したTask型のインスタンスで管理できるようになります。

それではDelayAsyncメソッドを実行している間に14行目で何か別の処理を行っておきましょう。一通り別の処理が終わったところで、先ほど取得しておいたTask型のインスタンスに対してawait演算子を使うことで、そのタスクがまだ終わっていなければメインスレッドを待機させます。これにより、時間のかかる処理を行っている最中に他の処理を行うことが可能になりました。さらにここでは2つのDelayAsyncメソッドをそれぞれ同時に別スレッドで行っているので、2秒間の待機が同時に行われており、プログラムの終了までは2秒で済んでいます。

処理の非同期メソッド化

戻り値を持たないメソッド

戻り値を持たないメソッドを非同期メソッドとする場合は以下のように戻り値をTask型の非同期メソッドとします。

基本書式
async Task VoidMethodAsync()
{
    // 処理
}

なお、C# 7.1よりMainメソッドも以下のように非同期メソッドとすることが可能になりました。

static async Task Main(string[] args)
{
    // 処理
}

戻り値を持つメソッド

戻り値TResultを持つメソッドを非同期メソッドとする場合は以下のように戻り値をTask<TResult>型の非同期メソッドとします。

基本書式
async Task<TResult> RetMethodAsync()
{
    // 処理
}

イベントハンドラー

イベントハンドラーは基本的に戻り値はvoidであり、それはそのイベントハンドラーを登録するイベントごとにデリゲート型で定義されているものなので、非同期処理を行うからと言ってTask型の戻り値を持つことはできません。そのため例外的に次のように非同期メソッドの戻り値をvoidとする必要があります(イベントハンドラー以外で非同期メソッドの戻り値をvoidとすることは避けてください)。

基本書式
async void SomeEvent(object sender, EventArgs e)
{
    // 処理
}
コード例:ボタンをクリックしたときのイベント
private async void Button_Click(object sender, RoutedEventArgs e)
{
    // 処理
}

関連記事・スポンサーリンク

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)