はじめに
今回の記事は一応以下の記事の続きです。
また、内容はすべて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キーはサイトのサンプルのものを使っています。
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()
で囲ってあげています。
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クラス。
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クラスの処理を以下のように書き換えることができます。
// 書き換え前
var response = await Task.Run(() => (HttpWebResponse)request.GetResponse())
// 書き換え後
var response = await (HttpWebResponse)request.GetResponseAsync()
また、今回やりたいことに関しては、HttpClientクラスを使うことで、もっと簡単に処理を書くことができます。
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()
というメソッドがあるので、それを使うことができます。
// 書き換え前
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()とか使ってるよ......ま、まぁそこは本質じゃないから...(震え)
正直今更感のある内容ですが、誰かの助けになれば幸いです。
参考サイト
-
[C#5.0~] async/awaitとTask.Runメソッドを用いた非同期処理のメモ
- コンソールアプリ、GUIアプリでのTask.Run(), async/awaitの振る舞いの違いについて
-
usingを使え、使えったら使え(^^)
- 例外発生時のリソース開放忘れを防ぐため、Dispose()できるオブジェクトはusing句が使えるか検討しようという話
-
WebRequest Class
- WebRequestの使い方と、HttpClientが使えるならそっちが推奨という公式ドキュメント
-
非同期WebRequestとTimeout処理の今昔
- タイムアウトが厄介だからWebRequestじゃなくてHttpClientを使おうなという話
-
.NETのHttpClientの取り扱いには要注意という話
- HttpClientでusing句を使っちゃだめだぞという話