初心者のためのTask.Run(), async/awaitの使い方

# はじめに

最近、Unity/C#でもちょいちょい非同期処理を書いているんですが、なんだかんだやっぱり非同期は難しいです。  

そこで、GoogleでTaskやasync/awaitについて検索すると、「TaskをWait()するのはダメ！」とか「async voidは基本的に使っちゃダメ！」といった記事はよく見かけるのですが、<b>じゃあどう書けばいいんじゃ</b>と思って探してみると、なかなか記事が見つかりません。見つかっても、「UIのクリックイベントにasync voidのメソッドを登録して...」という記事が多いです。
残念ながら僕はUnityユーザなので、コードを書いてて非同期処理をしたいとき、処理のきっかけはUIのクリックイベントではない時の方が多く、あまり参考にすることができませんでした。

ということで、UnityでTask, async/awaitを使おうとしたときに僕が困ったことや、その結果わかってきた使い方をできるだけ具体的に書いていこうと思います。
何個かブログを書くかもしれませんが、今回の主役は`Task.Run()`です。

なお、マサカリは大歓迎です。

# 関連記事

この記事を読む前に、以下におすすめの記事を載せます。読んだことがない人は事前に読んでみてください。
なお、今回の記事ではUniRxについては触れていきません。

[Taskを極めろ！async/await完全攻略](https://qiita.com/acple@github/items/8f63aacb13de9954c5da)

# Task.Run()
さて、なにかとよく出てくる`Task.Run()`ですが、こいつは一体何をしてくれる関数なのでしょうか。
詳しくは[この記事](https://qiita.com/acple@github/items/8f63aacb13de9954c5da)に書いてありますが、ざっくり言ってしまうと、「**同期処理（もともと非同期処理ではなかった処理）**をまとめてタスクを作り、**別スレッドで実行する**」関数です。

個人的には、以下の３つの使い方を押さえておけば大体何とかなると思います。

- 重い同期処理（返り値なし）を非同期にしたい場合
- 重い同期処理（返り値あり）を非同期にしたい場合
- 非同期で無限ループを回したい場合

# 1. 重い同期処理（返り値なし）を非同期にしたい場合
まず簡単な例として、以下のような重い処理が必要になったとします。

```CS:非同期処理が必要な重い処理（返り値なし）
private void HeavyMethod(string str)
{
    // 何か重い処理
    Thread.Sleep(1000);

    // 重い処理をした続きの処理
    SomethingNextMethod(str);
}

private void SomethingNextMethod(string str)
{
    // 何か続きの処理
    Debug.Log(str);
}
```

よく見るサンプルコードですね。
このとき、`HeavyMethod()`の中には`await`を使う処理がないものとします。

このままUnityのメインスレッドで、`HeavyMethod()`を呼ぶと、画面がガッと止まってしまうので、重い処理の部分を`Task.Run()`で別スレッドで処理を行ってあげる必要があります。
具体的にはこうです。

```CS:HeavyMethod()を別スレッドで走らせるメソッド
public void HogeHoge()
{
    Task.Run(() => HeavyMethod("hoge"));
}
```

実をいうと個人的にはこの書き方は好きではないのですが、こうすれば`HogeHoge()`メソッドを発動することで、画面のフリーズを避けることができます。

## Task.Run()の落とし穴

...が、実はこのコードには少し問題があります。
それは、`SomethingNextMethod()`以降の処理が行われるスレッドに関してです。
最初に言ったとおり、`Task.Run()`は引数として与えられた処理を**別スレッドで**実行します。つまり、本来別スレッドで行ってほしくない`SomethingNextMethod()`の処理まで別スレッドで行われてしまうということです。これはメソッドだけでなく、`Task.Run()`内で発火したイベントでも同様のことが起こります。とんだ落とし穴です。

「別スレッドで処理が続くと何がダメなの？」と思うかもしれませんが、実は「メインスレッドからしか実行できないメソッド」というのはいくつも存在します。個人的な経験だと、IBMのWatsonで音声認識を開始するときのメソッドがそうでした。

で、これを何とかするためには、**SynchronizationContext**というものを使う必要があります。
僕の記事では詳しく紹介できませんが、@toRisouP さんの[こちらの記事](https://qiita.com/toRisouP/items/a2c1bb1b0c4f73366bc6)に分かりやすく書いてあります。
このオブジェクトを使えば、自分の指定したスレッドで処理を走らせることができます。現在の処理が走っているスレッドは`Thread.CurrentThread.ManagedThreadId`で確認することができるので、ちょっと以下のコードをUnityで走らせて確認してみましょう。

```CS:実行スレッドの確認
/// <summary>
/// メインスレッドに処理を戻すためのオブジェクト
/// </summary>
private SynchronizationContext _mainContext;

private void Start()
{
    _mainContext = SynchronizationContext.Current;

    Hoge();
}

public void Hoge()
{
    Debug.Log($"Main thread ID : " + Thread.CurrentThread.ManagedThreadId);

    Task.Run(() => HeavyMethod());
}

public void HeavyMethod()
{
    // 何か重い処理
    Thread.Sleep(1000);

    // そのままメソッドを続けて書いた場合
    SomethingNextMethod("Hoge");

    // メインスレッドに処理を戻したい場合
    _mainContext.Post(_ => SomethingNextMethod("Fuga"), null);
}

private void SomethingNextMethod(string str)
{
    Debug.Log($"[{str}] Thread ID : " + Thread.CurrentThread.ManagedThreadId);
}
```

結果は、以下のようになりました。
![スクリーンショット (33).png](https://qiita-image-store.s3.amazonaws.com/0/222054/debd0b30-55f5-fd1a-2776-0472dda0bfed.png)
**SynchronizationContext**を使って実行スレッドを指定しなかった場合では、処理が別スレッドで走ってしまっていることがわかると思います。ちなみに、僕が実行したときは`Task.Run()`でスレッド番号92が使われていますが、`Task.Run()`は内部で`スレッドプール`という仕組みを使っているため、毎回実行スレッドが変わります。

スレッド関係のバグは気づかないところで起きてたりもするので、特に意図がない場合はSynchronizationContextを使って、処理はメインスレッドに戻したほうが良いでしょう。


# 2. 重い同期処理（返り値あり）を非同期にしたい場合

前述の落とし穴を見ていて、こう思った方もいるかもしれません。

「`Task.Run()`でメソッドを発動させるのが少し面倒なら、`HeavyMethod()`で値を返すようにして、`HogeHoge()`内で続きの処理を書けばいいのでは？」と。

はい。詳しくは後述しますが、実は`await`修飾子を使うことで、スレッドを意識せずに処理をつなげて書くことができます。
ということで、次に`HeavyMethod()`に返り値がある場合を見てみましょう。
具体的には以下のようなメソッドがあり、`HeavyMethod()`の処理を実行した後に、その結果を使って何かしらの処理を続けたいとします。

```CS:非同期処理が必要な重い処理（返り値あり）
private string HeavyMethod(string str)
{
    // 何か重い処理
    Thread.Sleep(1000);

    return str + "fuga";
}
```

これを非同期で処理する場合は、以下のように書きます。

```CS:HeavyMethod()のみを別スレッドで走らせるメソッド
public async Task HogeHogeAsync()
{
    // 重い処理を非同期で実行し、その結果を得る
    string result = await Task.Run(() => HeavyMethod("hoge"));

    // 重い処理をした続きの処理
    SomethingNextMethod(result);
}

private void SomethingNextMethod(string str)
{
    Debug.Log(str);
}
```

ようやくasync/awaitが出てきました。こいつらがやっていることは以下の通りです。

- `async`
    - メソッド内で`await`修飾子を使えるようにする
- `await`
    - `Task.Run()`で作成したタスクの実行が終了するまで、`HogeHogeAsync()`メソッドの処理を止める  
     （元のスレッドをフリーズさせるわけではない）
    - タスクが終了した場合は、`Task.Run()`が返してくる`Task<string>`からstringを抜き出して返し、**元のスレッドで**`HogeHogeAsync()`内の処理を続行する  
     （ただしUnity上での挙動 - 参考：https://www.slideshare.net/UnityTechnologiesJapan/unite-tokyo-2018asyncawait）

この`await`の働きのおかげで、実行スレッドを手動でもとに戻してあげる必要がなくなりました。
このように、`await`は使う側がスレッドや`Task`を意識せずに処理を続けて書けるようにしてくれる構文なんですね。

よっしゃ、勝ったな。非同期完全に理解した。
あとは`HogeHogeAsync()`を使うだけ......

...ん？

## 落とし穴その２（HogeHogeAsync()を使ってみる）

`HogeHogeAsync()`を別のメソッドから使うと、こんな警告が出てきました。
![スクリーンショット (38).png](https://qiita-image-store.s3.amazonaws.com/0/222054/88119f6c-ad56-75f7-d340-7a81c19d4ec4.png)
えっ、なにこれは。
とりあえず実行は出来るみたいだけど、気味が悪いな......なんとかしよう。

### 試行その１：`await`を追加してみる
Visual Studioさんに`await`つけろって言われてるんなら、素直につけてみましょう。
![スクリーンショット (39).png](https://qiita-image-store.s3.amazonaws.com/0/222054/995cb576-3027-3233-2155-4a5b705dbf3c.png)
警告どころかエラーになりました。
内容は「`await`使ってるのに`FugaFuga()`メソッドに`async`修飾子がついてないよ」というもの。
なんだ、じゃあ`async`つければいいや。
![スクリーンショット (41).png](https://qiita-image-store.s3.amazonaws.com/0/222054/90366202-84f7-7fee-2f05-f8e3bb02a369.png)
よし、警告は消えたな。

~~...ん？~~

~~ああああ、**`async void`**！！、**`async void`**じゃないか！！！~~
~~こりゃだめだ、しかもこれ結局`FugaFuga()`を使うメソッドまで、無限に`async`をつけなきゃいけないじゃないか！~~

<font color='red'>`FugaFuga()`が別スレッドから呼ばれない限り、これでいいです。
`FugaFuga()`が別スレッドから呼ばれるなら、`HogeHogeAsync()`内の例外が握りつぶされないように、ちゃんとエラーハンドリングして`SynchronizationContext`の`Post()`メソッドで例外をメインスレッドに投げ直してあげてください（2020/5/10 修正）</font>

### 試行その２：`HogeHogeAsync()`を改変してみる
`await`を一度使うと、それを使う側まですべて`async/await`修飾子をつけなきゃいけない...（思い込み）
じゃあ`await`を一度も使わなければいいのでは？？？

そういえば`Task`には`task.Result`ってプロパティがあったな...
`await`ではなくこれを使えばいいのでは？？？

```CS:絶望のデッドロックコード
private void FugaFuga()
{
    HogeHoge();
}
    
public void HogeHoge()
{
    // Taskはawaitせず直接受け取って
    var task = Task.Run(() => HeavyMethod("hoge"));

    // task.Resultで直接、値にアクセスする
    var result = task.Result; // ここで結局スレッドが止まる
    SomethingNextMethod(result);
}
```

~~はい、デッドロックです。~~
~~実は`task.Result`の内部では`Task`を`Wait()`する処理が走ります。~~
~~そのとき、なぜデッドロックが発生するかは過去にたくさんの記事が書いてあるので見てみてください。~~

<font color='red'>`Task.Run()`の中の処理は別のスレッドプールで走るため、`task.Result`でメインスレッドを止めてもデッドロックはしませんが、結局処理が完了するまでメインスレッドが止まってしまうため、非同期処理ではなくなってしまいます。
これはC#組み込みの`async Task`メソッドでも同じ挙動になりますが、実行時のスレッドでタスクの完了を待ち受ける`async Task`メソッドで`task.Result`すると、タスクの完了を待ち受けるスレッドを停止してしまうことになり、デッドロックするので注意です。（2020/5/10 修正）</font>

### じゃあどうするか

<font color='red'>エラーハンドリングさえちゃんとしとけば試行1のやり方でいいです（2020/5/10 修正）</font>

~~試行１、試行２はダメでした。~~
~~じゃあどうすればいいかというと、こう書くことです。~~

```CS:問題なくasyncメソッドを使う方法
private void FugaFuga()
{
    var task = HogeHogeAsync();
}
    
public async Task HogeHogeAsync()
{
    string result = await Task.Run(() => HeavyMethod("hoge"));

    SomethingNextMethod(result);
}
```
~~追加されたのは、`FugaFuga()`メソッドの中で、`HogeHogeAsync()`を`await`せずに、ただの`Task`を変数`task`に代入する処理だけです。これで警告が消えます。~~

~~そのあとの処理には全く影響がないのに、なぜこれで警告が消えるのでしょうか。~~
~~そのヒントがこのページに書いてあります。~~

~~[async、awaitそしてTaskについて（非同期とは何なのか）](https://hiroronn.hatenablog.jp/entry/20171005/1507207811)~~

~~以下引用です~~
> - ~~同期処理~~
>     - ~~処理が終わるまで待つ~~
> - ~~非同期処理~~
>     - ~~処理が終わるまで待つかどうかを、**利用者に委ねる**~~

~~ここに書いてある通り、`async`のついたメソッドは利用者側で、処理を**待つ**ことも、**待たない**こともできます。
また、[Taskを極めろ！async/await完全攻略](https://qiita.com/acple@github/items/8f63aacb13de9954c5da)には、以下のようにも書いてあります。~~

> ~~本来、Taskは好きなときに好きなようにWaitしても全く問題ないものだった~~

~~つまり、本来はこういう書き方ができたはずだったのでしょう。~~
~~実際、`ConfigureAwait(false)`を使えば以下のようなコードでもデッドロックを回避できます。~~

```CS

private void FugaFuga()
{
    // HogeHogeAsync()のタスク(Task<string>)をそのまま受け取り
    var task = HogeHogeAsync();

    // taskの完了を待つ
    task.Wait();

    // taskの処理は終了しているので、Resultを参照
    SomethingNextMethod(task.Result);
}
    
public async Task<string> HogeHogeAsync()
{
    return await Task.Run(() => HeavyMethod("hoge"));
    // return await Task.Run(() => HeavyMethod("hoge")).ConfigureAwait(false); // これならデッドロックしない
}
```
~~エディタの立場に立ってこれを加味すると、「あとで`Wait()`するかもしれないんだし、すぐに`await`しないにしてもとりあえず`Task`は受け取っておきなよ。処理投げっぱなしは良くないよ」という気持ちになるのでしょう。~~
~~これが、あの「`この呼び出しを待たないため、現在のメソッドの...`」という警告の原因ではないかと思います。~~

余談ですが、僕は「タスクを変数`_`として受け取っておき、その後一切処理をしない」ことを、暗黙的に「そこの非同期処理は完了待ちをしない」ことを明示する目印としています。
なので僕は、**1.重い処理に返り値がない場合**のような処理を書く時も、以下のような書き方を好んでいます。

```CS
// 特に問題ないけど、あまり好きではない
Task.Run(() => HeavyMethod("hoge"));

// C# 6.0ならこう
var _ = Task.Run(() => HeavyMethod("hoge"));

// C# 7.0以降ならこう ... discards (値の破棄)
_ = Task.Run(() => HeavyMethod("hoge"));
```

複数のタスクを複雑に組み合わせて一つのタスクを作りたいときは、何か意味のある名前を付けてあげればいいと思います。

# 3. 非同期で無限ループを回す

最後に非同期で無限ループを回したいときの使い方です。
使いどころとしては、自前でオレオレネットワークライブラリを書いてたりすると使う必要が出てきます。
例えば以下のコードは、クライアントとTCP通信するサーバ側の処理の雛形です。

```CS:TcpServer.cs
/// <summary>
/// クライアントと非同期で受信・送信処理を開始する
/// </summary>
public void Communicate()
{
    _communicateLoop = true;

    var _ = Task.Run(() => 
    {
        while (_communicateLoop)
        {
            try
            {
                // クライアントとの送信・受信処理
            }
            catch (Exception e)
            {
                Debug.LogWarning("[Server] " + e);
            }
        }
    });
}
```

`Task.Run()`の中にラムダ式ですべてのコードが書いてあるだけで今まで書いてきたことと特に違いはなく、無限ループなので処理の待ち受けをしていません。
もし`while`の中からイベントを発火するようなことがあれば、**SynchronizationContext**の使用を考えるのも**1.重い処理に返り値がない場合**と同様です。

# さいごに

今回の記事では、`Task.Run()`を使った基本的な非同期処理について書いていきましたが、別に「非同期処理といえば`Task.Run()`」という訳では**全く**ありません。
むしろ`async/await`だけで事足りることの方が多いと思いますし、個人的にも無限ループくらいにしか使いません（だいたい非同期処理したい時にはデフォルトで`await`できるメソッドが存在する）。

しかし、`Task.Run()`と`async/await`の関係がよく分からず、一時期混乱していたので今回の記事を書こうと思いました。
今回の内容が、誰かのお役に立てば幸いです。

もし気力が残ってたら、次回は`Task.Run()`を使わない非同期処理や、`Task`そのものについて書けたらと思います。

最後まで読んでいただきありがとうございました。

- 追記
    - Unityのawaitの挙動について補足（2018/12/06）  
      参考：https://www.slideshare.net/UnityTechnologiesJapan/unite-tokyo-2018asyncawait
    - [Task.Run()ではなく用意されている非同期メソッドを使おう](https://qiita.com/Alupaca1363Inew/items/edcc70f0abf9273e2397)（2019/09/16）
        - 無駄なTask.Run()を消そうという記事をアップロードしました