40
35

More than 3 years have passed since last update.

Taskじゃない非同期をTaskに変換したい(TaskCompletionSource)

Last updated at Posted at 2019-10-28

はじめに

Task(UniTask)は便利です。
特にこの昨今awaitで結果を待てるので手続き処理っぽく書きつつ非同期処理が書けます。

でも、 Taskじゃない非同期処理はどうしましょう。

例えばボタンのクリック。 ボタンのクリックはユーザーが気が向いたときにするものなので非同期です。

PlayFabやNCMBといったmBaasのAPI呼び出しなどは通信中画面が止まっては困るのでコールバックで結果(もしくはエラー)を受け取るようになっています。 これもまた非同期です。

こういった「非同期だけれど、Taskではない」

をTaskに変換してくれる便利な奴がいます。 それが TaskCompletionSource(UniTaskCompletionSource)です。

※この記事では基本Task/TaskCompletionSourceを使いますが、適宜UniTask/UniTaskCompletionSourceに読みかえてください

基本形

public Task<int> GetIntAsync(){
    var tcs = new TaskCompletionSource<int>();



    return tcs.Task;
}

intを返すTask Task<int> に対応するのは TaskCompletionSource<int> ですし
floatを返すTask Task<float> に対応するのは TaskCompletionSource<float> です。

この返却したいTaskに対応した TaskCompletionSource を new して、 Task をreturn するのが基本形です。

しかし、このメソッドのTaskはいつまでたっても結果を返しません。 それはそう。 誰も結果を返してないからです。

結果のセット

結果を返すにはこのTaskCompletionSourceのインスタンスに TrySetResult を呼ぶ必要があります。
(なお、キャンセルする場合は TrySetCanceled()を呼ぶ必要がありますが、それはまたの機会に・・・)

public Task<int> GetIntAsync(){
    var tcs = new TaskCompletionSource<int>();

    tcs.TrySetResult(32768);//結果のセット

    return tcs.Task;
}

public async Task RunAsync(){
    var num = await GetIntAsync(); 
    Debug.Log(i); //→ 32768 
}

これで、このメソッドはTaskの結果として32768を返すようになります。
さて。これは非同期でしょうか。違いますね。 これでは return Task.FromResult(32768); と同じです。

しかし、ここで一つとても重要なことがあります。それはこの TrySetResultは非同期で呼んでも構わないということです。

と言われても良くわからない方もいるでしょう。
例えば。 最初に例で出した ボタンのクリック を絡めてみます。

例1 指定したボタンをクリックしたらintを返却するTask

まず、基本形を作ります

public Task<int> ButtonClickAsync(){
    var tcs = new TaskCompletionSource<int>();

    return tcs.Task;
}

「指定したボタンを」とあるので、引数でボタンを受け取ります

public Task<int> ButtonClickAsync(Button btn){
    var tcs = new TaskCompletionSource<int>();

    return tcs.Task;
}

ボタンがクリックされたら、結果を返却するようにします。 結果を返却するには TaskCompletionSourceTrySetResult を呼べば良いのでした。

public Task<int> ButtonClickAsync(Button btn)
{
    var tcs = new TaskCompletionSource<int>();

    btn.onClick.AddListener(() => tcs.TrySetResult(65536));//ボタンクリックイベントのコールバックで結果セット

    return tcs.Task;
}

完成です。これで、ボタンをクリックするとこの tcs.Task は 65536 を返却して終了するようになります。
簡単ですね。無事 Taskじゃない非同期処理をTask化できました

※これだとbtnのonClickイベントが登録されっぱなしじゃないか!と思った貴方。大正解です。本当はちゃんと解除してあげてください。

では、次はほんのちょっと応用です。 (今となってはObsoleteな)WWWクラスをTask化してみます

例2 指定したURLからページ情報を取得するTask

これまた、基本形を作ります。 ページ情報なので Task は Task<string> TaskCompletionSourceも TaskCompletionSource<string> にします。

public Task<string> WWWAsync(){
    var tcs = new TaskCompletionSource<string>();

    return tcs.Task;
}

「指定したURLから」とあるので、引数でurlを受け取ります

public Task<string> WWWAsync(string url){
    var tcs = new TaskCompletionSource<string>();

    return tcs.Task;
}

WWWクラスはコルーチンで結果を待つ仕組みなので、StartCoroutineで指定したurlからWWWでコンテンツを取得するようにします。

public Task<string> WWWAsync(string url){
    var tcs = new TaskCompletionSource<string>();

    StartCoroutine(WWWGet(url));

    return tcs.Task;
}

private IEnumerator WWWGet(string url)
{
    var www = new WWW(url);
    yield return www;
    //wwww.text; //TODO ここにダウンロードしたコンテンツが入っている
}

結果を返却するには TaskCompletionSourceTrySetResult を呼べば良いのですが、コルーチンの結果は(基本的には)取得できないので、WWWGetの引数に TaskCompletionSource<string> を追加して、WWWGetの中で結果を入れてもらうように修正します。

public Task<string> WWWAsync(string url)
{
    var tcs = new TaskCompletionSource<string>();

    StartCoroutine(WWWGet(url,tcs));

    return tcs.Task;
}

private IEnumerator WWWGet(string url, TaskCompletionSource<string> tcs)
{
    var www = new WWW(url);
    yield return www;
    tcs.TrySetResult(www.text);
}

完成です。 コルーチンによる非同期もTaskにすることが出来ました。

合体!

完全に蛇足ですが、上記二つの例を組み合わせると、「ボタンがクリックされるまで待ち、クリックされたら指定されたURLにアクセスしてコンテンツを取得する」Taskが作れます。

    private async Task RunAsync()
    {
        var i = await ButtonClickAsync(_btn);//指定したボタンがクリックされるのを待つTask
        Debug.Log(i);

        var content = await WWWAsync("http://yahoo.co.jp");//指定したURLのコンテンツを取得するTask
        Debug.Log(content);
    }

んー。すっきりしてますね!

最後に

今回は非常にシンプルな例を挙げましたが、使ってみるとっかかりにでもなれば幸いです。

Taskにしておくと結果が取得しやすいのはもちろんのこと、Task.WhenAnyTask.WhenAllを使って、複数あるうちのどれか一つのTaskが完了待ちや全てのTask完了待ちなど、ただの非同期処理では難しかった非同期処理同士の連携が簡単にできるようになったりするので(状況に応じて)Task化進めるのも良いと思います。

補足

  • 基本形として TaskCompletionSource をローカル変数で作りましたが、メンバ変数でも構いません。 というか、その方が便利な場合も多々あります。 ただし、TaskCompletionSourceの使いまわしはできません。 1回 SetResultしたらそれで終わりなので、また同様のTaskを呼ぶ場合は TaskCompletionSource を再インスタンス化する必要があります。
  • ボタンクリックや、wwwの待ち等は実は UniRX.Async を使っていればこんなことせずに await が可能です。 (ボタンクリック待ちのTaskは .OnClickAsync(); が用意されています)今回の内容はあくまでも TaskCompletionSource の使い方の簡単な説明なのであしからず。
  • TaskCompletionSource には、結果を返さないただのTaskがありません。 そのため別に返却する値が無い場合でも TaskCompletionSource<object> とし、 TrySetResult(null); とやることがよくあります。 が、 UniTaskCompletionSource には非Generics版が用意されているので、返却値が無い場合はそちらを使用しましょう。
  • イベント周りはUniRX使った方が適切なケースも多いです。適宜使い分けましょう。

よい。Taskライフを。

40
35
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
40
35