0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

もう怖くない!Unityでの Task / async-await の基礎と実践

Last updated at Posted at 2025-05-21

修正予定箇所

  • バウンド処理関係をもう少し詳しく記載する

はじめに

通信関連の非同期処理に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
async Task ReadDataAsync()
{
    // 重いデータ読み込み 
};

await

  • await Play();→Play()が終了するまで待ってから次の行を実装する
  • 指定したTaskの完了を待つ
  • await した行より下の処理は、そのTaskが終わるまで実行されない
非同期 await
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. 呼び出しもとで例外がキャッチできない
  2. 非同期処理が完了したかどうかが分からない(状態を監視できない)
  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」というサフィックスをつけるというものがあります。
ここ

awaitがついていない

awaitがついていることで非同期処理の結果を待機しているということが明示的に分かります。

Unityの非同期の処理

皆さんご存じの通りUnityのAPIはシングルスレッド設計つまり、
メインスレッドのみで動作し、メインスレッド以外では動作できません。

危険な例
void Start()
{
    Task.Run(() => MyTask()); // 別スレッドで開始
}

async Task MyTask()
{
    // 別スレッドでUnity関連の参照をいじると例外が発生する
    // デバッグログも出力されない
    Debug.Log("プレイヤー座標:" + transform.position);
}

解決策は単純で
別スレッドで動作する処理はUnityAPIを使用せずに作成する
というものです。

_ = OnProcess();の「_」てなんだよ

「_」は値の破棄を明示的に行うものということらしいです。
ここ

最後に

C#,Unityともに習熟中ですので誤りがありましたら是非指摘お願いいたします。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?