放浪軍師のアプリ開発局

VTuberみたいなアプリケーション夏狂乱など、自由気ままにアプリを開発したりしています。他にもいろいろやってます。尚、このブログはわからないところを頑張って解決するブログであるため、正しい保証がありません。ご注意ください。

非同期処理をもうちょっと理解したい

放浪軍師のXamarin.Formsによるアプリ開発
今回は非同期処理をもうちょっと理解しようというお話です。
まず、初めて訪問された方は以下をお読みください。

www.gunshi.info

重い処理を行っているときにメッセージが欲しい

先日の展示会でBluetoothやソケット通信での無線計測を行ったのですが、その時どうしても再現したかったのに出来なかった事がありました。
それは、非同期処理
通信にはどうしても時間がかかるので、その際に通信中とか、しばらくお待ちくださいとかの表示が欲しかったんですが、結局諦めてしまいました。
以前乱ちゃん作成時にもアドバイスでなんとかしましたが、あの時も理解できている気がしなかったし展示会用でも苦戦してしまったので、ちょっと学んでみることにしました。

色々なページを見るも今一理解が出来ない

非同期処理については、そもそもXamarinというよりC#の内容ですので記事は多いですね。色んな方が解説してくださっています。、わかりません…。書いてある意味がよくわかりません…。少しはC#も慣れてきたとは思うんですけどまだまだです。

…ま、これはいつものことなんですけどね!こういう時はとりあえず実験してみるのが一番いいのです。イクゾー!

とりあえず思いつく限り書いてみよう

様々な解説サイトを巡って出てきたキーワードやコードを元に、やりたいことを実現すべく実験用アプリを作成しました。
見た目はこんな感じ。
f:id:roamschemer:20180812010859p:plain:w300

其々のボタンに片っ端からそれっぽいコードを打ち込んで挙動を確認していきます。
起きて欲しい挙動はこんな奴です。
f:id:roamschemer:20180812011624g:plain:w300

ボタンを押すと重い処理の前に起動中ですと表示し、重い処理が完了したら起動しましたと表示する。こんな挙動が出来るようなコードを探し当てます。

実験だ!

まず重い処理を行うメソッドを作成します。こんな感じ。

private void Omoisyori()
{
    //BluetoothOpen等の非常に重い処理。
    Thread.Sleep(3000);
}

3秒かかるような処理を仮想的に作成しています。では、実験開始!

1.とりあえず書いてみる
private void Command1()
{
    MessageText.Value = "起動中です";
    Omoisyori();
    MessageText.Value = "起動しました";
}

f:id:roamschemer:20180812021830g:plain:w300
当然ですがダメですね。起動中は表示されません。3秒後に起動しましたと表示するだけです。

2.なんかそれっぽく書いてみる
private void Command2()
{
    MessageText.Value = "起動中です";
    Task task = Task.Run(() => Omoisyori());
    MessageText.Value = "起動しました";
}

f:id:roamschemer:20180812021931g:plain:w300
なんかTask.Runを使えば出来るらしいというのは知っていたので書いてみたがダメでした。ボタンを押したら即起動しましたと表示してしまいます。

3.じゃあTask.Runの位置を変えればいけるんじゃね?
private void Command3()
{
    Task task = Task.Run(() => MessageText.Value = "起動中です");
    Omoisyori();
    MessageText.Value = "起動しました";
}

f:id:roamschemer:20180812021952g:plain:w300
いけるかと思ったがダメでした。これも3秒後に起動しましたと表示するだけでした。

4.考えを変えて以前教えてもらった方法でやる
private async void Command4()
{
    MessageText.Value = "起動中です";
    await Task.Delay((int)(1));
    Omoisyori();
    MessageText.Value = "起動しました";
}

f:id:roamschemer:20180812022017g:plain:w300
思い通りの挙動はします。が、コレジャナイ感満載。

5.Task.Factory.StartNewってのでいけるらしいぞ?
private void Command5()
{
    Task task = Task.Factory.StartNew(() =>
    {
        MessageText.Value = "起動中です";
        Omoisyori();
    });
    MessageText.Value = "起動しました";
}

f:id:roamschemer:20180812022043g:plain:w300
とりあえず使ってみたが謎の動きです。押すたびに挙動が変化します。なんだこれ。

6.task.Waitが必要みたいだぞ
private void Command6()
{
    Task task = Task.Factory.StartNew(() =>
    {
        MessageText.Value = "起動中です";
        Omoisyori();
    });
    task.Wait();
    MessageText.Value = "起動しました";
}

f:id:roamschemer:20180812022118g:plain:w300
task.Wait();を使うと、そこまでが完結するまで挙動を止める効果があるらしいとあったので使ってみましたがダメでした。何かが違う…

7.やぶれかぶれ
private void Command7()
{
    MessageText.Value = "起動中です";
    Task task = Task.Factory.StartNew(() =>
    {
        Omoisyori();
    });
    task.Wait();
    MessageText.Value = "起動しました";
}

f:id:roamschemer:20180812022143g:plain:w300
こう書けば起動中ですの表示と重い処理を同時に行って、task.Waitで再開すると思ったんですがダメでした。この辺で適当な理解ではダメだなと気が付き調べまくりました。

8.そもそも重い処理の書き方に問題があるのでは?

色々調べたところ、重い処理側の書き方に問題があると気が付く。Thread.Sleepではダメなのでは?と変更。

private async Task Omoisyori2()
{
    //BluetoothOpen等の非常に重い処理。
    await Task.Delay(3000);
}

として、

private async void Command8()
{
    MessageText.Value = "起動中です";
    await Omoisyori2();
    MessageText.Value = "起動しました";
}

f:id:roamschemer:20180812022208g:plain:w300
ここでやっと再現出来ました!
awaitとは、そのメソッドが終わるまでは待ちますという宣言です。そしてその待ってる間に起動中のような表示を行ってくれるようですね。ん?…だがちょっと待って欲しい。そもそも今回はBluetoothなんかで通信が重くなるようなメソッドを処理したいのであって、Task.Delayを何とかしたいわけじゃない。もうちょっと実験だ!

9.重い処理をウェイトを挟まない形で再現してみる
private async Task Omoisyori3()
{
    long j = 0;
    //非常に重い処理はTask.Runで囲むと別スレッドで処理してくれる。
    await Task.Run(() =>
    {
        for (long i = 0; i < 999999999; i++)
        {
            j = i;
        }
    });
}

凄いループを使う事で重い処理を再現しました。そしてこの辺でスレッドの意味を確認しました。Task.Run()で囲むことによって別スレッドで処理、つまり並列処理が可能になります。
そして

awaitを置かない場合は非同期処理。
→実行は別スレッドに任せて、元のスレッドは待たずに先に進めてしまう。
awaitを置く場合は同期処理。
→実行は別スレッドに任せて、元のスレッドは処理を一時中断。実行中の処理だけは完結させて待機。

となる事が理解出来ました。これだよこれが知りたかったんだよ!…いや、色んなサイトに書いてはあるんだけどね。実際にこういった短い処理を書いてみないとわかんないよね。じゃあ、呼び出し元はこう書けばいいって事です。

private async void Command9()
{
    MessageText.Value = "起動中です";
    await Omoisyori3();
    MessageText.Value = $"起動しました";
}

f:id:roamschemer:20180812022308g:plain:w300
これでバッチリ!完成しました。
あれ?でもそれなら…

10.別スレッドを用いる場合、全部このパターンでいけるんじゃね?
private async void Command10()
{
    MessageText.Value = "起動中です";
    await Task.Run(() => Omoisyori());
    MessageText.Value = $"起動しました";
}

f:id:roamschemer:20180812011624g:plain:w300
いけました。多分大抵のパターンに対応できるんじゃないかと思います。

まとめ

並列処理させたい場合は、

Task.Run(() => Omoisyori());

と書いて、非同期ならそのまま。同期ならawaitを付ける。

で大体いけるんじゃないかなぁ…?ただ、いろんな解説を見てみるとこの非同期処理関係はもっともっと奥深いみたいです。そこはまぁまた行き詰った時にまた調べればいいさ!