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

[UniRx.Async] UnityWebRequestAsyncOperationConfiguredAwaiter周辺で困った話

More than 1 year has passed since last update.

CancelしてもUnityWebRequestはAbortされない

UniTaskにはUnityWebRequestをawaitできる機能が付いていて、さらにそれをキャンセルできるインターフェースが用意されている。
こんな感じ。

CancellationTokenSource cts;

public async void SendRequest()
{
    cts = new CancellationTokenSource();
    var req = UnityWebRequest.Get("http://example.com");

    await req.SendWebRequest().CongigureAwait(cts.Token);
    Debug.Log("Completed: " + req.downloadHandler.text);
}

public void Cancel()
{
    if (cts != null) cts.Cancel();
}

が、これでキャンセルしてもUnityWebRequestはAbortされない
AwaiterのGetResultでOperationCanceledExceptionがthrowされるので、await以降の処理は実行されない。(UnityAsyncExtensions.cs#L640-L642
が、UnityWebRequestはAbortされていないので、例えば以下の場合に普通に通信完了時にレスポンステキストがログに出力される。(await使うのにわざわざこう書くことは無いと思われるが)

CancellationTokenSource cts;

public async void SendRequest()
{
    cts = new CancellationTokenSource();
    var req = UnityWebRequest.Get("http://example.com");

    var asyncOp = req.SendWebRequest();
    asyncOp.completed += _ => {
        Debug.Log("UnityWebRequest completed");
        Debug.Log("Completed: " + req.downloadHandler.text);
    };
    await asyncOp.CongigureAwait(cts.Token);
    Debug.Log("After await");
}

public void Cancel()
{
    if (cts != null) cts.Cancel();
}

なので、通信も終了させたい場合はcts.Cancel()と同時にUnityWebRequestもAbortするように気を付けておかないといけない。

わざわざIssueも投げてみたけど、よくよく考えてみればあくまで「UnityWebRequestAsyncOperation」をAwaitしてる訳で、UnityWebRequestをAwaitしてる訳ではないので、Abortされないのはむしろ自然な挙動だったと気付いた。(「通信処理の完了を待機する」のをキャンセルするので「通信処理」自体はキャンセルされないという感じ)
…という事で、仕様でいいんですよね?

Progress.ReportがContinuationの後に実行される

どういうことかというと、以下のコードはNullReferenceExceptionを発生させる。

[SerializeField] Text _text;

public async void StartRequest()
{
    var req = UnityWebRequest.Get("http://example.com");
    await req.SendWebRequest().ConfigureAwait(new Progress<float>(x => _text.text = x.ToString()));
    Debug.Log("StartRequest Completed");
    GameObject.Destroy(_text);
    _text = null;
}

image.png

何故かというと、await後の処理(Continuation)が行われた後にProgressのReportが実行されている
上記コードではawait後に_textをDestroyしている為、その後にProgressのReportをしようとして_textを参照してNullReferenceExceptionになるという流れ。

UniTaskの内部ではこの辺でProgressのReportとcontinuationのinvokeを行っている
UnityAsyncExtensions.cs#L656-L666

if (progress != null)
{
    progress.Report(asyncOperation.progress);
}

if (asyncOperation.isDone)
{
    this.result = asyncOperation.webRequest;
    InvokeContinuation(AwaiterStatus.Succeeded);
    return false;
}   

一見するとIProgress.ReportのあとにcontinuationがInvokeされているので問題無いように見える。が、この問題の本質はProgressクラスそのものにある。
Progressクラスは簡単に言うと、コンストラクタでSyncronizationContextを取得し、Report時にはSyncronizationContext.Postでその処理を投げるという仕組みになっている。

SyncronizationContextって何?という方はこちら
【Unity開発者向け】「SynchronizationContext」と「Taskのawait」 - Qiita

なので、Progress.Reportを呼んだ段階ではSyncronizationContext内のキューに積まれるだけで、実際に実行されるタイミングは分からないという話。
Unityの実装がどうなってるかいまいちわからないけど、多分1フレームごとのライフサイクルのどっかでDequeueされる。
参考:UnitySynchronizationContext.cs#L41-L61
なので、1フレーム遅らせると解決したりする。

var req = UnityWebRequest.Get("http://example.com");
await req.SendWebRequest().ConfigureAwait(new Progress<float>(x => _text.text = x.ToString()));
await UniTask.DelayFrame(1);
Debug.Log("StartRequest Completed");
GameObject.Destroy(_text);
_text = null;

が、そもそもProgressでSynchronizationContextが使われているのが問題なので、標準のProgressを使わなければ良い。(ConfigureAwaitで求められるのはIProgressなので、必ずしもProgressでなくて良い)
で、SynchronizationContextを使わないProgressが既にUniRx.Asyncの中に組み込まれている。
Progress.cs

var req = UnityWebRequest.Get("http://example.com");
await req.SendWebRequest().ConfigureAwait(Progress.Create<float>(x => _text.text = x.ToString()));
Debug.Log("StartRequest Completed");
GameObject.Destroy(_text);
_text = null;

これでNullReferenceExceptionは発生しなくなる。めでたしめでたし。

まとめ

  • UnityWebRequestは必要なら手動でAbortしよう
  • 標準のProgressは使わず、UniRx.AsyncのProgress.Createを使おう
k7a
Game & Web application Engineer
Why not register and get more from Qiita?
  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
No 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
ユーザーは見つかりませんでした