Help us understand the problem. What is going on with this article?

Task.Run()ではなく用意されている非同期メソッドを使おう

はじめに

今回の記事は一応以下の記事の続きです。
また、内容はすべてUnity上での話になります。

初心者のためのTask.Run(), async/awaitの使い方

前回の記事では、Task.Run()を使って、同期処理を非同期処理にする基本的な例を挙げました。今回はその補足としてもう少しだけ実践的な処理を書いてみようと思います。
内容としては、あとがきでチラッと書いた「Task.Run()を使う必要がない場面」として、ネットワーク処理やファイルのI/O処理の具体的な例を挙げていきます。

想定する読者はC#での非同期処理を始めたての方々です。

WebリクエストとIO処理の非同期処理

例えば、天気予報API(OpenWeatherMap)を使ってデータを取得し、それをJSONファイルに保存する処理を書くとします。
非同期&非同期ですね。

作るクラスはWeatherForecast, DataLogger, AsyncSampleの3つです。

ブロッキング処理をTask.Run()で囲ってみる

WeatherForecastクラスは、WebRequestクラスを使ってエンドポイントにアクセスし、データを取得する処理で書いてみましょう。あとで解説しますが、ここではあえてTask.Run()を使って処理を書いています。
URLやAPIキーはサイトのサンプルのものを使っています。

WeatherForecast
public class WeatherForecast
{
    private const string END_POINT = "https://samples.openweathermap.org/data/2.5/weather?q=";
    private const string APP_ID = "b6907d289e10d714a6e88b30761fae22";

    /// <summary>
    /// データが欲しい地点を受け取り、結果を返す非同期メソッド
    /// </summary>
    /// <param name="location"></param>
    /// <returns></returns>
    public static async Task<string> RequestWeatherAsync(string location)
    {
        string url = END_POINT + location + "&appid=" + APP_ID;

        // WebRequestの作成
        var request = WebRequest.Create(url);

        try
        {
            // WebRequestを使ってデータを取得。GetResponse()はブロッキング処理
            using (var response = await Task.Run(() => (HttpWebResponse)request.GetResponse()))
            using (var dataStream = response.GetResponseStream())
            using (var reader = new StreamReader(dataStream))
            {
                return reader.ReadToEnd();
            }
        }
        catch (Exception e)
        {
            Debug.LogWarning($"[ERROR] {e}");

            return "ERROR";
        }
    }
}

DataLoggerはこんな感じ。WriteLine()はブロッキング処理なのでTask.Run()で囲ってあげています。

DataLogger
public class DataLogger
{
    /// <summary>
    /// 保存するデータとパスを受け取り、データを非同期で保存する
    /// </summary>
    /// <param name="filePath"></param>
    /// <param name="jsonText"></param>
    /// <returns></returns>
    public static async Task<bool> SaveJsonAsync(string filePath, string jsonText)
    {
        try
        {
            // データを非同期でファイルへ書き込み
            using (FileStream fs = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
            using (StreamWriter writer = new StreamWriter(fs, System.Text.Encoding.UTF8))
            {
                await Task.Run(() =>
                {
                    writer.WriteLine(jsonText);
                });
            }
        }
        catch (Exception e)
        {
            Debug.LogWarning($"[ERROR] {e}");
            return false;
        }

        return true;
    }
}

最後に、それらを使うAsyncSampleクラス。

AsyncSample
public class AsyncSample : MonoBehaviour
{
    private string _location = "London";

    private async void OnGUI()
    {
        // ボタンを表示する
        var x = Screen.width / 3;
        var y = Screen.height / 2;
        var width = Screen.width / 3;

        if (GUI.Button(new Rect(x, y + 100, width, 40), "ボタン"))
        {
            // 指定の地域の天気を取得
            //var response = await WeatherForecast.RequestWeatherAsync(_location);
            var response = await WeatherForecast.RequestWeatherHttpAsync(_location);

            // "WeatherData.json"という名前のファイルでAssets直下にデータを保存
            var path = Application.dataPath + $@"\WeatherData.json";
            var succeed = await DataLogger.SaveJsonAsync(path, response);

            // 結果を出力
            var text = succeed ? "成功" : "失敗";
            Debug.Log(text);
        }
    }
}

これで表示されたボタンを押すと、メインスレッドは止まることなく問題なく処理が実行されます。

しかし実は、今回やったことと同じ処理がTask.Run()を使わなくても書けちゃいます。

用意されている非同期メソッドを使おう

天気予報で使ったWebRequestクラスですが、実はGetResponse()だけではなくGetResponseAsync()というメソッドを持っています。
このメソッドはawaitすることができるので、WeatherForecastクラスの処理を以下のように書き換えることができます。

WeatherForecast.RequestWeatherAsync()メソッド内
// 書き換え前
var response = await Task.Run(() => (HttpWebResponse)request.GetResponse())

// 書き換え後
var response = await (HttpWebResponse)request.GetResponseAsync()

また、今回やりたいことに関しては、HttpClientクラスを使うことで、もっと簡単に処理を書くことができます。

WeatherForecast
public class WeatherForecast
{
    private const string END_POINT = "https://samples.openweathermap.org/data/2.5/weather?q=";
    private const string APP_ID = "b6907d289e10d714a6e88b30761fae22";

    // HttpClientはstaticなものを使いまわす
    private static HttpClient _httpClient = new HttpClient();

    /// <summary>
    /// データが欲しい地点を受け取り、結果を返す非同期メソッド
    /// </summary>
    /// <param name="location"></param>
    /// <returns></returns>
    public static async Task<string> RequestWeatherHttpAsync(string location)
    {
        string url = END_POINT + location + "&appid=" + APP_ID;

        try
        {
            return await _httpClient.GetStringAsync(url);
        }
        catch (Exception e)
        {
            Debug.LogWarning($"[ERROR] {e}");

            return "ERROR";
        }
    }
}

コードがだいぶ短くなりましたね。リソースの開放忘れも起きなさそうです。

ではDataLoggerの方はどうでしょうか。
こちらも同様に、StreamWriterクラスにはWriteLineAsync()というメソッドがあるので、それを使うことができます。

DataLogger.SaveJsonAsync()メソッド内
// 書き換え前
using (FileStream fs = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
using (StreamWriter writer = new StreamWriter(fs, System.Text.Encoding.UTF8))
{
    await Task.Run(() => 
    {
        writer.WriteLine(jsonText);
    });
}

// 書き換え後
using (FileStream fs = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite,
    bufferSize: 4096, useAsync: true)) // useAsync=true もしくは options=FileOptions.Asynchronous
using (StreamWriter writer = new StreamWriter(fs, System.Text.Encoding.UTF8))
{
    await writer.WriteLineAsync(jsonText);
}

うん、いい感じにすっきりしましたね。
これで、書き換え前と同じように、メインスレッドをフリーズさせることなくI/O処理を行うことができます。
こんな感じで、普段よく使う非同期処理には、大体awaitできるメソッドが用意されています。特に理由がない場合は、そういったメソッドを優先的に使っていけばいいと思います。

Task.Run()の使いどころって?

今までの話の流れで行くと、Task.Run()の使いどころはかなり限られてきそうです。
前回の記事にも書きましたが、正直思いつくのは非同期の無限ループ処理くらいでしょうか。

ただ、状況によってはスレッドプールを意識し、Task.Run()を使うことで処理を効率化できる場面があるようです。
I/O待ちのためのTaskとバックグラウンド処理のためのTask

しかし基本的には、特に理由がない限りはTask.Run()を書く前に、やりたい処理に対応する非同期メソッドが存在しないかまずチェックしてみるのがいいと思います。

あとがき

この記事自体は1年前とかに書いていたのに、なぜか下書きのまま放っていたことに最近気づいたので投稿しました。
なんかOnGUI()とか使ってるよ......ま、まぁそこは本質じゃないから...(震え)

正直今更感のある内容ですが、誰かの助けになれば幸いです。

参考サイト

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away