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

ずっとコルーチン使ってたけど、Async・Awaitをちゃんと理解してみる (その1)

前提

  • Unity 2018.4.11f1
  • この記事は、個人的な探求の過程を記録したものです。内容は、記録時点での私の理解であり、必ずしも真実ではありません。

研究の範囲

  • Async Awaitとは何なのか?
  • できること、できないこと。
  • コルーチンの代替がどこまでできるか?

ドキュメント

  • async (Microsoft C# リファレンス)
  • Task Class (Microsoft .NET Framework)

基礎的なこと (執筆開始時点の理解)

  • namespace using System.Threading.Tasks;を使用します。
  • Aysync(エイシンク)、Await(アウェイト)みたいな読み方です。
  • Taskを返す非同期メソッドは、IEnumeratorを返すメソッド(コルーチン)みたいに、呼び出しがブロックされません。
    • 呼び出し時点でメソッドの処理が完了していなくても呼び出し側に制御が戻ります。
    • IEnumeratorのように「必ず」ではなく、即座に完了して戻る場合もあるようです。
    • メソッドの呼び出しにawaitを付けると、メソッドの完了まで待つ(同期する)ようになります。
      • コルーチンをUpdate中のMoveNextで回したみたいな感じでしょうか。
    • Task<T>を返すメソッドに付けられたawaitの型は、Taskが外れてTになります。
  • Awaitを使いたければ、メソッドの宣言にAsyncを付けてTaskTask<T>を返さなければなりません。
  • Asyncを付したメソッドは、一般に、何も返さないかTask型を返すようにします。
    • voidだとawaitを付けられません。そこはStartCoroutineと同じような感じですね。
    • Taskならawaitが付けられるけど、値を返せません。
  • await Task.Delay (int millisec)で、yield return new WaitForSeconds (float sec)のようなことができます。
    • あくまでもfloatで指定したい場合は、await Task.Delay (TimeSpan.FromSeconds (float sec))のようにします。
  • Task.Run ()を使うと、別スレッドが走るので、Task.Run ()の先では、Unityの一部の機能は使えなくなります。
    • Task.Factoryを使うのは古いやり方です。(.NET Framework 4.xが使えない場合)

試行と理解

デッドロック

Task<int> task;
void Start () {
    task = TestAsync ();
}

void Update () {
    Debug.Log (task.Result);
}

async Task<int> TestAsync () {
    Debug.Log ("begin task");
    await Task.Delay (1000); // 1秒待機
    Debug.Log ("end task");
    return 1;
}

TaskのプロパティにResultを見つけて、安易に上記のようなことをしたらエディターが応答しなくなりました。

void Start () {
    var task = TestAsync ();
    Debug.Log (task.Result);
}

async Task<int> TestAsync () {
    Debug.Log ("begin task");
    await Task.Delay (1000); // 1秒待機
    Debug.Log ("end task");
    return 1;
}

こう書いても同じです。

ドキュメントをきちんと読むと、Resultは、メソッドの処理が完了して結果が出るまでブロックされるようです。
つまり、以下のように書いても同じようにデッドロックが生じるのでしょうか。
やってみると、やはりエディタが応答しなくなります。

void Start () {
    var task = TestAsync ();
    task.Wait ();
    Debug.Log ("done");
}

async Task TestAsync () {
    Debug.Log ("begin task");
    await Task.Delay (1000); // 1秒待機
    Debug.Log ("end task");
}

これって、awaitとどう違うの?
以下のように、awaitを使って書けば、デッドロックは生じません。

async void Start () {
    var task = TestAsync ();
    await task;
    Debug.Log ("done");
}

async Task TestAsync () {
    Debug.Log ("begin task");
    await Task.Delay (1000); // 1秒待機
    Debug.Log ("end task");
}

awaitWait ()はどう違うのでしょうか?
無限に待つWait ()の代わりに、指定時間だけ待つWait (int millisec)ようにして、変化を探ります。

void Start () {
    Debug.Log ("start");
    var task = TestAsync ();
    task.Wait (2000); // 2秒間taskの完了を待つ
    Debug.Log ("done");
}

async Task TestAsync () {
    Debug.Log ("begin task");
    await Task.Delay (1000); // 1秒待機
    Debug.Log ("end task");
}

この結果は以下のようになります。

start
begin task
~ 2秒 ~
done
end task

次に、内外の待ち時間を逆にして試します。

void Start () {
    Debug.Log ("start");
    var task = TestAsync ();
    task.Wait (1000); // 1秒間taskの完了を待つ
    Debug.Log ("done");
}

async Task TestAsync () {
    Debug.Log ("begin task");
    await Task.Delay (2000); // 2秒待機
    Debug.Log ("end task");
}

この結果は以下のようになります。

start
begin task
~ 1秒 ~
done
~ 1秒 ~
end task

これらの試行から、次のようなことが解ると思います。

  • 長い方の待ち時間を超えないことから、双方の待機時間が平行して経過していること。(Task.Delayは別スレッドみたいですね)
  • doneend taskより先であることから、メイン側が待つことをやめて制御を返すまで、メソッド側が完了できないこと。

以上によって、以下のような理解を得ることができました。

  • awaitは、以降の処理をコルーチン終了時にコールバックで処理するように登録して、自身を終えて制御を返すようなもの (※)
  • Wait ()は、コルーチンを呼んだ後で、コルーチン終了待ちのループをするようなもの?
void Start () {
    StartCoroutine (TestAsync (() => Debug.Log ("done")));
}

IEnumerator TestAsync (Action onCompleted) {
    Debug.Log ("begin task");
    yield return new WaitForSeconds (1f); // 1秒待機
    Debug.Log ("end task");
    onCompleted ();
}

こう考えると、時間指定なしのWait ()の使いどころが思いつかないですね。
…もしかして、終了待ちのループでコルーチンが止まらなければ使える?
以下のように別スレッドで走らせれば、デッドロックしないのでしょうか。

void Start () {
    Debug.Log ("start");
    var task = Task.Run (() => TestAsync ()); // 別スレッドで実行
    task.Wait ();
    Debug.Log ("done");
}

async Task TestAsync () {
    Debug.Log ("begin task");
    await Task.Delay (1000); // 1秒待機
    Debug.Log ("end task");
}

確かにそのようです。
以下の結果が得られました。

start
begin task
~ 1秒 ~
end task
done

ただし、メインルーチンが最初から最後まで制御を戻さないので、ログは全て一息に表示されます。

あるいは、これでもいけるようです。

void Start () {
    Debug.Log ("start");
    TestAsync ().Wait ();
    Debug.Log ("done");
}

async Task TestAsync () {
    Debug.Log ("begin task");
    await Task.Delay (1000).ConfigureAwait (false); // 1秒待機
    Debug.Log ("end task");
}

通常のawaitは、.ConfigureAwait (true)になっているようです。
ドキュメントを読むと、trueだと、続きの処理を大元の環境で実行しようとして、falseだと環境を戻さないということらしいです。
元の環境に戻りたくても、元の環境では終了待ちループ(Wait)が居座って制御を離さない、非同期処理が終わらないからWaitが終わらない、斯くしてデッドロックが生じるわけですね。

この環境というのはスレッドなのでしょうか?
だとしたら、明示的に指示しなくても、別スレッドで実行される可能性があるということになりますが…。

using System.Threading;

void Start () {
    Debug.Log ($"start {Thread.CurrentThread.ManagedThreadId}");
    TestAsync ().Wait ();
    Debug.Log ($"done {Thread.CurrentThread.ManagedThreadId}");
}

async Task TestAsync () {
    Debug.Log ($"begin task {Thread.CurrentThread.ManagedThreadId}");
    await Task.Delay (1000).ConfigureAwait (false);
    Debug.Log ($"end task {Thread.CurrentThread.ManagedThreadId}");
}
start 1
begin task 1
~ 1秒 ~
end task 156
done 1

確かに、別スレッドで実行されたようです。

別スレッドを明示した場合と比較してみましょう。

void Start () {
    Debug.Log ($"start {Thread.CurrentThread.ManagedThreadId}");
    var task = Task.Run (() => TestAsync ()); // 別スレッドで実行
    Debug.Log ($"done {Thread.CurrentThread.ManagedThreadId}");
}

async Task TestAsync () {
    Debug.Log ($"begin task {Thread.CurrentThread.ManagedThreadId}");
    await Task.Delay (1000); // 1秒待機
    Debug.Log ($"end task {Thread.CurrentThread.ManagedThreadId}");
}
start 1
done 1
begin task 189
~ 1秒 ~
end task 184

おや?
.ConfigureAwait (false)していないのに、awaitの前後が別スレッドになっていますね。

asyncawaitだけならどうでしょうか。

void Start () {
    Debug.Log ($"start {Thread.CurrentThread.ManagedThreadId}");
    var task = TestAsync ();
    Debug.Log ($"done {Thread.CurrentThread.ManagedThreadId}");
}

async Task TestAsync () {
    Debug.Log ($"begin task {Thread.CurrentThread.ManagedThreadId}");
    await Task.Delay (1000); // 1秒待機
    Debug.Log ($"end task {Thread.CurrentThread.ManagedThreadId}");
}
start 1
begin task 1
done 1
~ 1秒 ~
end task 1
  • asyncawaitだけなら、シングルスレッドで動作していますが、Task.Run ()はもちろん、.ConfigureAwait (false)を使った場合も、別スレッドで実行されるようです。
  • 別スレッドでも、Debug.Logは使えるようですね。

例外の伝播

Task.Run ()を使わずに、asyncawaitだけを使う分には、Taskクラスが完了待ちループawaitの中で例外を伝達してくれるようです。

async void Start () {
    Debug.Log ("start");
    var task = TestAsync ();
    await task; // 完了待機
    Debug.Log ("done");
}

async Task TestAsync () {
    Debug.Log ("begin task");
    await Task.Delay (1000); // 1秒待機
    throw new Exception (); // 例外発生
    Debug.Log ("end task");
}

この結果は以下のようになります。

start
begin task
~ 1秒 ~
Exception

しかし、メイン側で完了待ちループawaitしないと、例外が受け取れません。

async void Start () {
    Debug.Log ("start");
    var task = TestAsync ();
    // await task; // 完了待機
    Debug.Log ("done");
}

async Task TestAsync () {
    Debug.Log ("begin task");
    await Task.Delay (1000); // 1秒待機
    throw new Exception (); // 例外発生
    Debug.Log ("end task");
}

この結果は以下のようになります。

start
begin task
done

Taskでなくvoidの場合も、もちろん、同じ結果です。
と、思いきや、違ってました。

async void Start () {
    Debug.Log ("start");
    /*var task = */TestAsync ();
    // await task; // 完了待機
    Debug.Log ("done");
}

async void TestAsync () {
    Debug.Log ("begin task");
    await Task.Delay (1000); // 1秒待機
    throw new Exception (); // 例外発生
    Debug.Log ("end task");
}
start
begin task
done
~ 1秒 ~
Exception

このように、voidの場合は例外が受け取れます。
どうやら、Taskを受け取った以上はawaitしなさいと言うことでしょうか。

では、Task.Run ()の場合はどうでしょうか。

void Start () {
    Debug.Log ("start");
    var task = Task.Run (() => Test2Async ());
    await task;
    Debug.Log ("done");
}

async Task Test2Async () {
    Debug.Log ("begin task");
    await Task.Delay (1000); // 1秒待機
    throw new Exception (); // 例外発生
    Debug.Log ("end task");
}

Task.Run ()awaitすれば受け取れるようです。
しかし、並列に動かそうとしてawaitを外すと…

void Start () {
    Debug.Log ("start");
    var task = Task.Run (() => Test2Async ());
    //await task;
    Debug.Log ("done");
}

async Task Test2Async () {
    Debug.Log ("begin task");
    await Task.Delay (1000); // 1秒待機
    throw new Exception (); // 例外発生
    Debug.Log ("end task");
}

これだと、例外は受け取れません。
こちらは、voidにしてもだめなようです。
以下のようにしても同じです。

void Start () {
    Debug.Log ("start");
    Task.Run (async () => await Test2Async ());
    Debug.Log ("done");
}

async Task Test2Async () {
    Debug.Log ("begin task");
    await Task.Delay (1000); // 1秒待機
    throw new Exception (); // 例外発生
    Debug.Log ("end task");
}

でも、以下のようにすれば、ブロックされずに受け取れるようです。

void Start () {
    Debug.Log ("start");
    TestAsync ();
    Debug.Log ("done");
}

async void TestAsync () {
    var task = Task.Run (() => Test2Async ());
    await task;
}

async Task Test2Async () {
    Debug.Log ("begin task");
    await Task.Delay (1000); // 1秒待機
    throw new Exception (); // 例外発生
    Debug.Log ("end task");
}
start
done
begin task
~ 1秒 ~
Exception

間接的にawait Task.Run ()を使うことで並列に処理しつつ、awaitがメインスレッドで処理されるので、例外を受け取ることもできるということでしょうか。

じゃあ、これはどうでしょう?

void Start () {
    Debug.Log ($"start {Thread.CurrentThread.ManagedThreadId}");
    TestAsync ();
    Debug.Log ($"done {Thread.CurrentThread.ManagedThreadId}");
}

async void TestAsync () {
    Debug.Log ($"begin task {Thread.CurrentThread.ManagedThreadId}");
    await Task.Delay (1000).ConfigureAwait (false); // 1秒待機
    Debug.Log ($"continue task {Thread.CurrentThread.ManagedThreadId}");
    throw new Exception (); // 例外発生
    Debug.Log ($"end task {Thread.CurrentThread.ManagedThreadId}");
}
start 1
begin task 1
done 1
~ 1秒 ~
continue task 84
Exception

あれ?
別スレッドになるから、てっきりダメかと思ったけど、ちゃんと受け取れますね。
う~ん???

受け方\戻り値 void Task
await -
Task.Run
await Task.Run

マルチスレッド

シングルスレッドでマルチタスク

まず、複数の非同期メソッドをawaitなしに呼び出して、ループさせてみます。

void Start () {
    Debug.Log ("start");
    TestAsync (1);
    TestAsync (2);
    Debug.Log ("done");
}

async void TestAsync (int number) {
    var task = Test2Async (number);
    await task;
}

async Task Test2Async (int number) {
    Debug.Log ($"begin task {number}");
    for (var i = 0; i < 1000; i++) {
        Debug.Log ($"continue task {number} {i}");
    }
    Debug.Log ($"end task {number}");
}

シングルスレッドで実行されるので、一方のループが終わってから他方のループが始まります。

start
done
begin task 1
begin task 2
end task 1
done
continue task 1 0~999
end task 1
continue task 2 0~999
end task 2

マルチスレッドでマルチタスク

同じことを、複数のTask.Run ()により別スレッドで走らせてみます。

void Start () {
    Debug.Log ("start");
    TestAsync (1);
    TestAsync (2);
    Debug.Log ("done");
}

async void TestAsync (int number) {
    var task = Task.Run (async () => await Test2Async (number));
    await task;
}

async Task Test2Async (int number) {
    Debug.Log ($"begin task {number}");
    for (var i = 0; i < 1000; i++) {
        Debug.Log ($"continue task {number} {i}");
    }
    Debug.Log ($"end task {number}");
}

この場合は、ふたつのカウントが同時に進みます。
なお、task 1と2の処理順は、不確定になるようです。
また、エディタで実行を終了しても、カウントは継続します。
(カウント中にコンパイルが入るとエディタがフリーズします。)

別スレッドでの例外

例外を発生させてみます。

void Start () {
    Debug.Log ("start");
    TestAsync (1);
    TestAsync (2);
    Debug.Log ("done");
}

async void TestAsync (int number) {
    var task = Task.Run (async () => await Test2Async (number));
    await task;
}

async Task Test2Async (int number) {
    Debug.Log ($"begin task {number}");
    await Task.Delay (1000); // 1秒待機
    throw new Exception (); // 例外発生
    Debug.Log ($"end task {number}");
}

この結果は、以下のように例外を2回受け取ります。
メインスレッドでawaitすれば、別スレッドの例外も受け取れるようですね。

start
done
begin task 1
begin task 2
~ 1秒 ~
Exception
Exception

yield return new WaitWhile (Func<bool> predicate)

相当するものは見つけられていません。
普通にループを書いてしまうと、そのスレッドをブロックしてしまいますよね。
スレッドを解放しつつ、時々ポーリングするような…。
ああ、そういうことですね。

void Start () {
    Debug.Log ($"start");
    var task = TestAsync ();
    Debug.Log ($"done");
}

async Task TestAsync () {
    Debug.Log ("begin task");
    await WaitWhileAsync (() => UnityEngine.Random.Range (0, 10) != 0); // 賽の目が出るまで待機
    Debug.Log ("end task");
}

async Task WaitWhileAsync (Func<bool> predicate) {
    while (predicate ()) {
        await Task.Delay (33); // 1/30秒弱待機
    }
}

これでいけそうです。
直に書いても知れていますね。

StopCoroutine (coroutine)

タスクの停止は、コルーチンのような外部からの強制停止ではなくて、外部からの要求を内部で監視して、都合の良いタイミングで自ら終わるという感じです。

async void Start () {
    Debug.Log ($"start");
    var tokenSource = new CancellationTokenSource ();
    var token = tokenSource.Token; // 中断トークン
    var task = Task.Run (async () => await TestAsync (token), token);
    await Task.Delay (1000); // 1秒待つ
    tokenSource.Cancel (); // キャンセル
    Debug.Log ($"done");
}

async Task TestAsync (CancellationToken token) {
    Debug.Log ("begin task");
    for (var i = 0; i < 100; i++) {
        if (token.IsCancellationRequested) { // キャンセルされた
            Debug.Log ("canceled");
            return;
        }
        await Task.Delay (16); // 1/60秒弱待機
    }
    Debug.Log ("end task");
}
start
begin task
~ 1秒 ~
done
canceled

この例のようにポーリングするのでなく、トークンに対してコールバックを設定することもできます。

応用

簡単リモートアセット(Unity Addressable Asset System)を試してみた (その1) (Qiita) のコルーチンをタスクに置き換えると、例えば以下のようになります。

private void Start () {
    loader ();
}

private async void loader () {
    // ロード
    var textPrefab = await Addressables.LoadAssetAsync<GameObject> ("Prefabs/BottomText.prefab").Task;
    var imagePrefab = await Addressables.LoadAssetAsync<GameObject> ("Prefabs/FullScreenImage.prefab").Task;
    var spriteAssets = await Addressables.LoadAssetsAsync<Sprite> ("Sprites", null).Task; // ラベルを指定して一括ロード
    // エラーがないことを確認
    if (textPrefab && imagePrefab && spriteAssets != null && spriteAssets.Count > 0) {
        // プレファブからオブジェクトを生成
        var image = Instantiate (imagePrefab, transform).GetComponent<Image> ();
        var text = Instantiate (textPrefab, transform).GetComponent<Text> ();
        // スプライトを順に切り替え
        for (var i = 0; i < spriteAssets.Count; i = (i + 1) % spriteAssets.Count) {
            image.sprite = spriteAssets [i];
            text.text = $"{spriteAssets [i].name}  <size=20>© UTJ/UCL</size>";
            await Task.Delay (3000);
        }
    }
}

これは問題なく動作します。
ここで、スライドショウが一巡するのを待って次の処理がしたいとして、以下のように書き直したものとします。

private void Start () {
    loader ().Wait ();
    // 開始後の処理
}

private async Task loader () {
    // ロード
    var textPrefab = await Addressables.LoadAssetAsync<GameObject> ("Prefabs/BottomText.prefab").Task.ConfigureAwait (false);
    var imagePrefab = await Addressables.LoadAssetAsync<GameObject> ("Prefabs/FullScreenImage.prefab").Task.ConfigureAwait (false);
    var spriteAssets = await Addressables.LoadAssetsAsync<Sprite> ("Sprites", null).Task.ConfigureAwait (false); // ラベルを指定して一括ロード
    // エラーがないことを確認
    if (textPrefab && imagePrefab && spriteAssets != null && spriteAssets.Count > 0) {
        // プレファブからオブジェクトを生成
        var image = Instantiate (imagePrefab, transform).GetComponent<Image> ();
        var text = Instantiate (textPrefab, transform).GetComponent<Text> ();
        // スプライトを順に切り替え
        for (var i = 0; i < spriteAssets.Count; i++) {
            image.sprite = spriteAssets [i];
            text.text = $"{spriteAssets [i].name}  <size=20>© UTJ/UCL</size>";
            await Task.Delay (3000);
        }
    }
}

具体的には、loaderに対して、.Wait ()できるようにTaskを返すようにして、デッドロックしないように、await毎に.ConfigureAwait (false)を付けます。
さらに、スライドのループが一巡で終わるようにしています。
しかし、コンパイルは通りますが、期待した動作はしません。
一番の問題は、Unityの機能であるAddressables.LoadAssetAsyncはメインスレッドでしか動作しないということです。
最初のawait Addressables.LoadAssetAsync~.ConfigureAwait (false)によって、以降の処理が別スレッドに飛ばされます。
その結果、次のawait Addressables.LoadAssetAsync~でエラーが生じます。

以下のようなことならできますが、同期的に処理したい場合は難しいですね。

private async void Start () {
    await loader ();
    // 次の処理
}

private async Task loader () {
    // ~

コルーチンの場合でも、同様に以下のように書くことはできても、他の処理をブロックして待つのは難しいですね。

private IEnumerator Start () {
    yield return StartCoroutine (loader ());
    // 次の処理
}

Task拡張クラス

TaskEx.cs
/// <summary>タスク拡張</summary>
public static class TaskEx {

    /// <summary>休止間隔</summary>
    private const int Tick = 16;

    /// <summary>1フレーム待機</summary>
    public static Task DelayOneFrame => Task.Delay (Tick);

    /// <summary>条件が成立する間待機</summary>
    public static async Task DelayWhile (Func<bool> predicate) {
        while (predicate ()) {
            await Task.Delay (Tick);
        }
    }

    /// <summary>条件が成立するまで待機</summary>
    public static async Task DelayUntil (Func<bool> predicate) {
        do {
            await Task.Delay (Tick);
        } while (!predicate ());
    }

}

yield return null;await TaskEx.DelayOneFrame;
yield return new WaitUntil (() => flag);await TaskEx.DelayUntil (() => flag);
yield return new WaitWhile (() => flag);await TaskEx.DelayWhile (() => flag);

とりあえず

今回はここまでにします。いろいろなことが解りました。
行きつ戻りつ書いたのと、きちんと見直せていないので、信頼性は低いです。
また、解らないことが出てきたら、手直ししたり、続きを書くかも知れません。

おさらいとまとめ

  • namespace using System.Threading.Tasks;を使用します。
  • Aysync(エイシンク)、Await(アウェイト)みたいな読み方です。
  • Taskを返す非同期メソッドは、IEnumeratorを返すメソッド(コルーチン)みたいに、呼び出しがブロックされません。
    • 呼び出し時点でメソッドの処理が完了していなくても呼び出し側に制御が戻ります。
    • IEnumeratorのように「必ず」ではなく、即座に完了して戻る場合もあるようです。
    • メソッドの呼び出しにawaitを付けると、メソッドの完了まで待つ(同期する)ようになります。
      • awaitは、以降の処理をくくっておいて、コルーチンの終了時にコールバックされるように登録し、自身は終了して制御を返します。
      • Wait ()は、終了待ちループです。終了せず制御を返しません。
    • Task<T>を返すメソッドに付けられたawaitの型は、Taskが外れてTになります。
  • Awaitを使いたければ、メソッドの宣言にAsyncを付けてTaskTask<T>を返さなければなりません。
  • Asyncを付したメソッドは、一般に、何も返さないかTask型を返すようにします。
    • voidだとawaitを付けられません。そこはStartCoroutineと同じような感じですね。
    • Taskならawaitが付けられるけど、値を返せません。
  • await Task.Delay (int millisec)で、yield return new WaitForSeconds (float sec)のようなことができます。
    • あくまでもfloatで指定したい場合は、await Task.Delay (TimeSpan.FromSeconds (float sec))のようにします。
  • Task.Run ()を使うと、別スレッドが走るので、Task.Run ()の先では、Unityの一部の機能は使えなくなります。
    • Task.Factoryを使うのは古いやり方です。(.NET Framework 4.xが使えない場合)
  • asyncawaitだけなら、シングルスレッドで動作していますが、Task.Run ()はもちろん、.ConfigureAwait (false)を使った場合も、別スレッドで実行されるようです。
  • 別スレッドでも、Debug.Logは使えるようです。
  • 例外は、await越しに届きます。awaitを使わない場合は届きません。

参考

試行の途中で拝読した下の記事は大変分かり易く参考になりました。
【図解】C#のasync/awaitの内部挙動を理解する (Qiita)

この記事には出てきませんが、IProgress<T>を使った別スレッドの進捗をメインスレッドに伝達する仕組みについては、こちらを参考にさせていただきました。
Progressを使ってみた (ひょうろくだまらん)

どうもありがとうございました。

Why do not you register as a user and use Qiita more conveniently?
  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
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