修正予定箇所
- バウンド処理関係をもう少し詳しく記載する
はじめに
通信関連の非同期処理にTaskやasyncを使いまくって試行錯誤したのでそれの備忘録です。
誤った認識の箇所あればご指摘お待ちしております。
結論
待つだけの処理(通信・ファイル読み書き)
かつ、I/Oバウンド処理でスレッドを使用せずとも行える処理
→ async + await
重たい計算や処理、元々同期的処理(読み込みが遅い・止まりそう)
かつ、必ずスレッドを使用しないといけない処理(CPUバウンド処理)
→ Task.Run()
使うべき手法 | やりたい処理 |
---|---|
async/await | ネットワーク通信,小規模ファイル読み書き,データベースアクセスなど |
Task.Run() | ゲームAIの思考(A*など),大規模数値シミュレーション,大量データ圧縮 |
CPUバウンド,I/Oバウンドとは?
// TODO:表を追加
非同期処理/同期処理とは?
自分はここから分かってなかったので改めて。
非同期処理:ある処理の完了を待っている間、別の処理を平行して行うこと
→ロードしてる間にUIを描画したりする等
→重い処理でフリーズさせない
同期処理 :処理が順番に進む、いつもの処理
→プレイヤーを動かす等
→重い処理はフリーズする
async/awaitとは?
async
- メソッドのシグネチャとして使用する
- メソッド内で
await
を使用できるようになる - 非同期メソッドと呼ばれるが、これを書いただけで別スレッドで動作するわけではない
- 非同期メソッドには
ReadDataAsync
のように「Async」をサフィックスとしてつけると〇
余談
asyncは単なる装飾でコンパイル結果は通常メソッドと変わりません。awaitキーワードがasync追加前のコードを破壊しないようにという意図があるようですasync Task ReadDataAsync()
{
// 重いデータ読み込み
};
await
- await Play();→Play()が終了するまで待ってから次の行を実装する
- 指定したTaskの完了を待つ
- await した行より下の処理は、そのTaskが終わるまで実行されない
async Task ReadData()
{
UdpReceiveResult result = await receiveUdpClient.ReceiveAsync();
Debug.Log("終了しました");
};
じゃあどう書く?
void Start()
{
_ = OnProcess(); // 開始
}
async Task OnProcess()
{
await EndlessLoad(); // 無限ループ
}
async Task EndlessLoad()
{
while(true)
{
// 無限ループ
}
}
→await をメインスレッドで行い、メインスレッドで無限ループが走っているから
void Start()
{
Task.Run(() => OnProcess()); // 別スレッドで開始
}
async Task OnProcess()
{
await EndlessLoad(); // 別スレッドで無限ループ
}
async Task EndlessLoad()
{
while(true)
{
// 無限ループ
}
}
→別スレッドで無限ループが走っているから
Task.Run(() => OnProcess());
は絶対に別スレッドでOnProcessを動かしてね!という指示
await EndlessLoad();は別スレッドで待機しているのでメインスレッドは普通に動く
非同期=別スレッドで動作していると思っていた俺に
別スレッドで動作する場合は
- Task.Run()で書いたとき
- await Load(); ←この関数内部でTask.Run()や意図的にスレッド分けてた場合
これのみです。
じゃあawaitで待機してるときにどこで処理してんのよ?
このままでは動かないがネットワーク通信をしているとする
void Start()
{
_ = OnProcess();
Debug.Log("Start終了");
}
// ネットワーク接続を行う
async Task OnProcess()
{
IPAddress ipAddress = IPAddress.Any; // 任意のIPアドレスで待機
m_tcpListener = new TcpListener(ipAddress, tcpPort); // TCPリスナーを指定したポートで開始
m_tcpListener.Start();
// 接続要求があれば受け入れる
// *****↓ここだよここ↓*****
// ↓このawaitしてる時にこの関数はどこで処理されてんの?
m_tcpClient = await m_tcpListener.AcceptTcpClientAsync();
}
await m_tcpListener.AcceptTcpClientAsync();
は「だれかとのネットワーク接続を待っている」と一旦解釈してください
このAcceptTcpClientAsync
は誰かと接続出来たら接続出来たもの
を返します。
この接続出来たかどうかをawaitで待機してる訳です。
この待機の処理をしているのはスレッド上ではなくOSのI/Oシステム
で、
このOSのI/Oシステム
はスレッドを使用せずに、「誰かと接続したらAcceptTcpClientAsync
に教えるね~」ということをしてくれます。
つまりこうです、
順序 | 処理場所 | 概要 |
---|---|---|
1 | メインスレッド | Unity実行でStartが呼ばれる |
2 | メインスレッド | OnProcessが呼ばれる |
3 | メインスレッド | m_tcpListener.Start();まで |
4 | OSの非同期I/O通知機構(IOCP?) | awaitで誰かとの通信接続を待機する(AcceptTcpClientAsync) |
5 | メインスレッド | Debug.Log("Start終了")が呼ばれる |
? | メインスレッド | 通信接続できたらOSから通知が来てawaitから抜け出す |
このようにTask.Run()で別スレッドでわざわざ動作させるべきでない、async/awaitはI/Oバウンド(ファイル・ネットワーク等の待機)処理に適しているということらしいです。
これを別スレッドでやってるものかと思ってました...
Task.Run()
もともと同期的な処理を非同期で処理したい際にしようする
必ず別スレッドで処理を動作させる
また、メインスレッド以外で行う処理のことをバッググラウンド処理という
処理タイプ | 処理例 | 使用するもの | スレッドの扱い |
---|---|---|---|
I/Oバウンド | ネットワーク、ファイルI/O | async / await |
非同期にしてもスレッドを占有しない |
CPUバウンド | 数学計算、重いロジック | Task.Run(() => {...}) |
スレッドプールで実行され、スレッドを使う |
async void はなぜダメなのか?
基本どのサイトで検索してもasync void はダメとか、そもそもそうならないとか...
絶対にasync Task にしろとか...
そもそもasync void はマイクロソフトが非推奨しています。ここ
理由は大きく3つあります
- 呼び出しもとで例外がキャッチできない
- 非同期処理が完了したかどうかが分からない(状態を監視できない)
- 呼び出し元が非同期であるかどうかが分からない
1. 呼び出しもとで例外がキャッチできない
try-catchでエラーハンドリングする場合はtryの中でエラーが発生する必要がありますが、
async void
の関数はTaskのように関数内の処理の状態を監視することができないため
例のようにawaitで待機していない場合エラーをキャッチすることができません。
MyMethod内でエラーハンドリングすればいいのでは?
はい。おっしゃる通りです。
async Task MyMethod()
{
try
{
await Task.Delay(1000);
throw new Exception("エラー発生");
}
catch (Exception ex)
{
Debug.Log("MyMethod内でキャッチ: " + ex.Message);
}
}
void Start()
{
try
{
MyMethod(); // ここで例外が起きてもcatchされない
}
catch (Exception ex)
{
Debug.Log("キャッチできない: " + ex.Message);
}
}
async void MyMethod()
{
await Task.Delay(1000);
throw new Exception("これはキャッチされない例外");
}
async void Start()
{
try
{
await MyTask(); // これは例外をキャッチできる
}
catch (Exception ex)
{
Debug.Log("キャッチできない: " + ex.Message);
}
}
async Task MyTask()
{
await Task.Delay(1000);
throw new Exception("例外処理を発生");
}
2. 非同期処理の状態を監視できない
async void ではawaitでの待機ができないためその非同期処理の状態(進行中、成功、失敗)が分かりません。
void Start()
{
DownloadData(); // 完了するのを待てない
Debug.Log("ダウンロード終了後に実行したい処理"); // ← 先に実行されてしまう
}
async void DownloadData()
{
await Task.Delay(2000);
Debug.Log("ダウンロード完了");
}
3. 呼び出しもとが非同期か分からない
void Start()
{
LoadPlayerData(); // 非同期かどうか見た目ではわからない
Debug.Log("プレイヤーデータの読み込み完了後に実行したい処理");
}
async void LoadPlayerData()
{
await Task.Delay(2000);
Debug.Log("プレイヤーデータ読み込み完了");
}
理由は、
関数名にAsyncがついていない
、awaitがついていない
この2点です。
関数名にAsyncがついていない
.NETスタイルの慣例で、すべての非同期メソッド名に「Async」というサフィックスをつけるというものがあります。
ここ
Unityの非同期の処理
皆さんご存じの通りUnityのAPIはシングルスレッド設計つまり、
メインスレッドのみで動作し、メインスレッド以外では動作できません。
void Start()
{
Task.Run(() => MyTask()); // 別スレッドで開始
}
async Task MyTask()
{
// 別スレッドでUnity関連の参照をいじると例外が発生する
// デバッグログも出力されない
Debug.Log("プレイヤー座標:" + transform.position);
}
解決策は単純で
別スレッドで動作する処理はUnityAPIを使用せずに作成する
、
というものです。
_ = OnProcess();の「_」てなんだよ
「_」は値の破棄を明示的に行うものということらしいです。
ここ
最後に
C#,Unityともに習熟中ですので誤りがありましたら是非指摘お願いいたします。