スポンサーリンク

[C#]非同期メソッドの使い方 -Taskをawaitするasyncなメソッドです-

今回は非同期処理についてです。なんかかっこいい響きですよね。

ボタンをクリックしたら処理に時間がかかって画面が固まったことはありますか?
1つの処理で時間がかかる場合、その処理が完了するまで画面がフリーズして(応答しなくなって)しまいます。そんな時に時間のかかる処理を非同期で行うことで画面がフリーズしないようにすることができます。

この記事では非同期メソッドの使い方や自分で作成する方法を紹介します。

同期処理 と 非同期処理 と スレッド

まず、処理が並んでいてそれを順番に1つずつ行っていくのが同期処理です。
処理Aを開始して終了を待って、次に処理Bを開始して終了を待って…とやっていくのが同期処理です。
で、非同期処理の場合は、処理Aを開始して終了を待たずに処理Bを開始して・・という感じになります。

処理(命令)が並んで列みたいになったのをスレッド(Thread)といいます。
C#のプログラムは1つのスレッド(メインスレッドとかUIスレッドとか呼ばれる)で処理を行っています。UI(画面)を描画するのも、UIへの入力(ボタンのクリックとか)を受け付けるのも、時間のかかる重い処理を行うのも1つのスレッド内で順番にやっているので1つの処理で時間がかかると画面がフリーズしてしまうというのが起こります。
で、時間のかかる処理は別スレッドでやらせてしまおう!というのが非同期処理です。

C#では非同期メソッドというのがあってそれを使って非同期処理を行うことができます。

非同期メソッドとは?

ざっくり書くと非同期メソッドは「メソッドの中で非同期処理を開始したらいったんメソッドをぬけ、非同期処理が完了したらメソッドの続きから再開する」というメソッドです。
メソッドをいったん抜けるのでプログラムのメインスレッドを止めずに処理を行うことが出来ます。
わかりにくいかもですが順番にいきます。
ここで「C# 非同期」と検索するとよくでてくる「Taskasync / await」の登場です。

Taskについて

TaskはC#が用意している型(クラス)で、こいつのRunメソッドに非同期にしたい処理を詰め込んだメソッドを渡すと別スレッドで実行してくれます。Runメソッドを呼んだ時点で処理が非同期で開始されます。

RunメソッドはTask型の値を返す。

Task myTask = Task.Run(() => {
    //処理
    //処理
});

戻り値を返すメソッドも渡せる。その場合RunメソッドはTask<T>型の値を返す。

Task<string> myTask = Task.Run(() => {
    //処理1
    //処理2
    return "abc";
});

awaitについて

Task型の値の前にawaitを付けると「別スレッドで処理が開始されたらいったんメソッドを抜け、別スレッドの処理が完了したらまたメソッドに戻ってawaitの後ろの行から処理を再開する」という動きをするようになる。
C#標準で用意しているTaskを返すメソッドにも使える(後述)。

以下はボタンのクリックイベントの抜粋です。
6行目にawaitが使われています。6行目まで処理をしたら(タスクが別スレッドで開始されたら)いったんメソッドから抜けます(呼び出し元に処理を返す)、そしてタスクが完了するとawaitの後ろの行から処理が再開されます。その時に変数runResultには非同期で処理をしたメソッドの結果が入ります。
いったんメソッドを抜けるのでメインの処理(このボタンクリックを呼び出したところ)は止まらずに進むことができます。そうなることで画面がフリーズしなくなります。

private async void btnStart_Click(object sender, RoutedEventArgs e)
{
    //何かの処理1

    //awaitを付けて結果を取得
    string runResult = await Task.Run(() => {
        //処理1
        //処理2
        return "abc";
    });

    //何かの処理2
}

awaitを付けた式はRunメソッドに指定したメソッドの戻り値が返ってくるようになる。

//戻り値なし
await Task.Run(() => { 
    //処理1
    //処理2
});

//戻り値はstring
string runResult = await Task.Run(() => {
    //処理1
    //処理2
    return "abc";
});

こんな感じで前後でボタンの無効・有効化を入れて非同期処理が動いてる間はボタンを押せなくするとかが簡単にできる。

private async void btnStart_Click(object sender, RoutedEventArgs e)
{
    //ボタンを無効化
    btnStart.IsEnabled = false;

    //awaitを付けて結果を取得
    string runResult = await Task.Run(() => "abc");

    //ボタンを有効化
    btnStart.IsEnabled = true;
}

asyncについて

メソッド内でawaitを使うときはメソッドの宣言のところ(privateとかvoidのとこ、「シグネチャ」というらしい)にasyncを付ける必要がある。つけないとVisualStudioに怒られる。
これで非同期メソッドができあがり。

こんなメッセージ「’await’ 演算子は、非同期メソッド内でのみ使用できます。このメソッドに ‘async’ 修飾子を指定し、戻り値の型を ‘Task’ に変更することを検討してください。」がでるので、

asyncを追加

非同期メソッドの戻り値について

asyncを付けた非同期メソッドの戻り値は、voidTaskTask<T>のいずれかにする必要があります。voidはイベント用に用意されたので、自分で非同期メソッドを定義する場合、TaskかTask<T>にします。
違いは、値を返さない場合にTask、値を返す場合にTask<T>になります。
Tは戻り値の型を指定します。Task<int>、Task<string>とか

//戻り値なしの非同期メソッド
private async Task testMethodAsync()
{
    await Task.Run(() => { 
        //-- 非同期にする処理を書く --
    });
}

//戻り値あり(string型)の非同期メソッド
private async Task<string> testMethod2Async()
{
    return await Task.Run(() => {
        //-- 非同期にする処理を書く --

        //結果を返す
        return "abc";
    });
}

で、作った非同期メソッドAを呼び出すメソッドBもawaitを付けてメソッドAを待機して、そうすると宣言にasyncを付けてメソッドBも非同期メソッドになって・・・と非同期メソッドを使おうとするとasyncとawaitが付いて回ります。最終的にはイベントハンドラ(ボタンのクリックメソッドとか)までasyncを付けるか、途中のメソッドでawaitを使わずに非同期処理の完了を待たないかという感じになると思います。

呼び出すメソッド、呼ばれるメソッド両方が非同期メソッドの場合の処理の順番は、

//画面のボタンがクリックされたときに呼ばれる非同期メソッド(呼び出す側)
private async void btnStart_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("処理1_1");
    await testMethodAsync();
    Debug.WriteLine("処理1_2");
}

//非同期メソッド(呼ばれる側)
private async Task testMethodAsync()
{
    Debug.WriteLine("処理2_1");
    await Task.Run(() => {
        Debug.WriteLine("処理3_1");
    });
    Debug.WriteLine("処理2_2");
}
処理1_1
処理2_1
処理3_1
処理2_2
処理1_2

どちらも非同期の処理の終了を待って処理されてます。
で、呼び出す側が普通のメソッドの場合は、非同期メソッドを呼んだら終了を待たずに処理続行という感じの順番になります。

//画面のボタンがクリックされたときに呼ばれる普通のメソッド(呼び出す側)
private void btnStart_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("処理1_1");
    testMethodAsync();          //awaitを付けないで呼び出し
    Debug.WriteLine("処理1_2");
}

//非同期メソッド(呼ばれる側)
private async Task testMethodAsync()
{
    Debug.WriteLine("処理2_1");
    await Task.Run(() => {
        Debug.WriteLine("処理3_1");
    });
    Debug.WriteLine("処理2_2");
}
処理1_1
処理2_1
処理3_1
処理1_2
処理2_2

こちらは非同期メソッドの終了を待たずに処理(btnStart_Click)を抜けています。

.NET API標準の非同期用のメソッド

C#標準で時間のかかりそうな処理は非同期で処理できるようにメソッドが用意されています。

  • ファイルの読み書き
  • ネットワークのデータ送受信

などがあります、詳細はマイクロソフト公式ページを見てください。

async および await を使用したタスク非同期プログラミング (TAP) モデル (C#)
タスク ベースの非同期プログラミングを使用するタイミングと方法を学習します。これは、C# で非同期プログラミングを実行する簡単な方法です。

んで、System.Net.Http.HttpClientの定義を見てみると、

public Task<HttpResponseMessage> GetAsync(string? requestUri, HttpCompletionOption completionOption);
public Task<string> GetStringAsync(Uri? requestUri);

こんな感じでメソッド名の後ろに「~Async」と付いているのが非同期用のメソッドです。
戻り値がTask型なのでawaitを使って非同期で処理を行うことができます。

private async void btnStart_Click(object sender, RoutedEventArgs e)
{
    HttpClient client = new HttpClient();
    string htmlText = await client.GetStringAsync("https://google.com");
}

別スレッドからメインスレッドのUI要素にアクセスする

非同期処理から画面のUI要素をいじろうとすると別スレッドからはダメだよとおこられてしまいます。

別スレッドからDispatcherというメインスレッドの作業を管理しているオブジェクトに作業依頼してやってもらいます。あ、WPFの話ね。

private async void btnStart_Click(object sender, RoutedEventArgs e)
{
    await Task.Run(() => {
        Dispatcher.BeginInvoke(() => {
            this.lstMessage.Items.Add("非同期処理だよ");
        });
    });
}

コンソールアプリで非同期したいときは?

コンソールアプリの場合はMainメソッドを抜けるとプログラムが終了してしまうので、どこかで明示的に待機する必要があります。別スレッドで処理を開始してからメインスレッド側で何か処理をしてそれが終わったら別スレッドの終了を待機するという感じになります。
別スレッドの終了を待機するにはTaskオブジェクトのWaitメソッドかResultプロパティを使います。
非同期で開始したメソッドが戻り値なしの場合にWaitメソッド、戻り値がある場合にResultプロパティを使う。Resultプロパティは結果を取得するために間接的にWaitしていることになる。

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

class Program
{
    //Mainメソッドで別スレッドで何か処理をさせてその間に処理をする
    static void Main() 
    {
        Console.WriteLine("Program Start.");

        //別スレッドで処理を開始
        var t = Task.Run(() => {
            foreach (var i in Enumerable.Range(0, 5))
            {
                Thread.Sleep(1000);
                Console.WriteLine($"非同期スレッドCount:{i}");
            }
            return true;
        });

        //別スレッド処理中に行う処理を書くところ===================
        foreach (var i in Enumerable.Range(0, 5))
        {
            Thread.Sleep(1000);
            Console.WriteLine($"メインスレッドCount:{i}");
        }
        //===================================================

        //別スレッドの終了を待機
        var taskResult = t.Result;
        Console.WriteLine($"taskResult = {taskResult}");

        Console.WriteLine("Program End.");
    }
}
Program Start.
メインスレッドCount:0
非同期スレッドCount:0
メインスレッドCount:1
非同期スレッドCount:1
メインスレッドCount:2
非同期スレッドCount:2
非同期スレッドCount:3
メインスレッドCount:3
メインスレッドCount:4
非同期スレッドCount:4
taskResult = True
Program End.

こんな感じで同時に処理がうごいている。


長くなったので分けようと思います。次はスレッドのキャンセルとか複数タスクの実行とかについて書く予定。お楽しみに。


¥3,520 (2022/04/28 19:09時点 | Amazon調べ)
¥2,750 (2022/04/28 19:18時点 | Amazon調べ)

C# プログラミング講座に戻る

コメント