Continuity is The Father of Success

Androidアプリとかゲームとか。毎日続けてるものについて。

コードの読み方(時間編)

はじめに

プログラミング経験のない友人がUnityを始めたので、応援記事を書いていきます。

前回はこちら。

blog.dr1009.com

ソースコードは簡略なものにしています。 なお動作確認は .Net Fiddle を利用しています。

dotnetfiddle.net

今回のブログで目指すこと

  • 複雑な処理の組み合わせがイメージできるようになる
  • 複雑に処理を組み合わせの課題をイメージできるようになる

複雑な処理の組み合わせ

自分が書いたプログラム、他人が書いたプログラムのどちらにも「複雑なコード」と「単純なコード」があります。 次の2つのコードであれば後半の方が「複雑」そうに見えますが、よく見てみるとほぼ変わりません。

var result = 1 + 2;

Console.WriteLine(result);
var result1 = 1 + 2;
var result2 = result2 * 5;

Console.WriteLine(result2);

では、次のコードのようになるとどうでしょうか?

var client = new HttpClient();
var result = await client.GetStringAsync("http://www.example.com/");

Console.WriteLine(result);

行数としては変わりませんが、 HttpClient から GetStringAsync をしています。 これはHTTP GETリクエストを行い、その結果をコンソールに出力しています。 詳細はイメージできずとも、単純な 1 + 2 よりも 複雑 なことは把握できかると。


プログラムのコードは、一見「行数が短く」ても「関数にまとめられている」ために「短く見えているだけ」ということがあります。 これは処理をメソッドに分割し、人間にとって扱いやすくしていくと、実際に行われる処理が隠されていくためです。*1

プログラミングについて色々と調べていくと 計算量 という概念が出てきます。 計算量が少なければすぐ処理が終わり、多ければ時間がかかってから終わる、ぐらいのイメージを持っておくと良さそうです。

www.slideshare.net

すぐ終わる処理と長くかかる処理

よくある「すぐに終わらない処理」の話を少しだけします。 スマートフォンやPCを使っていて『処理を待つ』時間として何がイメージされるでしょうか? 非常によく『待つ』事例として、『アプリケーションのインストール』や『初めて訪れるサイトの読み込み』に思い至るのではないでしょうか?

これらの処理はもちろん計算処理もかかっているのですが、『HDD/SSDへの書き込み』や『サーバーからのデータ取得』などの待ち時間も発生しています。 こういった、「自分が書いたプログラムの速度に関係ない、外部の要因によって遅くなる」ことが存在します。 簡単にイメージすると、メモリ上の処理で完結しないような状態、HDD/SSDからのデータ読み込みや通信処理が発生している状態です。

同期処理と非同期処理

これまでに書いていたサンプルコードは、「上から下に、順々に」処理が進むことを前提としてきました。 これは同期処理と呼ばれます。 同期処理としてメソッドを呼び出す(記述する)と、そのメソッドが終わってから次の処理(行)に進んでいきます。

しかし「ある処理中に別の処理をしたい」ようなケース、例えば『サーバーからファイルをダウンロード中に、ダウンロードの進捗ゲージを更新する』ようなケースではどうでしょうか。 これは『サーバーからファイルをダウンロード』する処理と『ダウンロードの進捗ゲージを更新』する処理に分解されます。 2つの処理を「順々に」処理していると、ダウンロード側終わった後の進捗ゲージの更新しかできません。 このようなケースでは 同期処理 ではなく非同期処理 を利用します。


マイクロソフトの公式ドキュメントでも 非同期プログラミング として、解説されています。 この解説は丁寧に、そして抜けなく記述されているため、プログラミングに慣れてきたら読み込んでみることをお勧めします。 *2

docs.microsoft.com

複雑に処理を組み合わせていくとき

ここからは同期処理に非同期処理を組み合わせていく、そんなシチュエーションを考えていきます。

非同期処理を行うとき、特に理由なく使うと言うことはまずありません。 と言うのも、非同期処理を扱うのは(さまざまな言語でプログラマー向けのサポートが入っていても)同期処理よりも複雑で難解になるためです。

非同期処理を利用することで、便利になりやすいことをあげるとすれば、次の2つがあげられるのではないでしょうか。

  1. 実行時間(ユーザーを待たせる時間)を短くできる
  2. 画面表示を更新しつつ、計算処理を行う必要がある

かかる時間を考えてみる

非同期処理を利用すると、「はやく」処理を終わらせることができます。

これは日常生活の中では、ごく当たり前のことです。 例えばパスタを茹でる時、パスタソースを別の鍋で作ったり湯煎したりすることを思い出してみてください。

1つの鍋しかない場合、7分のパスタを茹で終わってから5分のソースを湯煎することになり、12分かかります。 しかし2つの鍋がある時には、7分のパスタを茹で始めるのと同時に別の鍋でソースを湯煎し、7分で終了できます。 *3

非同期処理を適切に利用すると、同じように「はやく」処理を完了させることができます。 「HDD/SSDからアセットを読み出す」処理と「サーバーにアプリが正規な手順で実行されているか確認する」処理がある時を考えてみます。 この2つの処理がほぼ一瞬、もしくは我慢できる程度の時間で終われば、同期処理で実装してしまっても問題は(ユーザーからのクレームは)ないでしょう。 しかし、現実的には『この2つの処理は、可能な限りはやく』終わらせるべきでしょう。 そうなれば「読み出しと、サーバーへの確認」は同時に行ったほうが良さそうです。

処理の順番を考えてみる

文章で非同期処理について書いてみると、非常に簡単なものに思えてきます。 これはヒトの生活の中では、ヒトが複数の工程を同時に走らせることを頻繁に行っているため、その管理を意識せずともできるためです。 *4

非同期処理として、工程を並列/平行に実行する箇所は、各プログラミング言語や実行環境がなんとかしてくれます。 プログラミングして書かなければいけないのは「どのように開始し、終了したらどうするか」になります。

前述のパスタの例では、「どれぐらい時間がかかるか」があらかじめわかっていました。 つまりパスタは7分でソースは5分、といった情報はあらかじめわかっていました。 そして予定されていた時間通り、茹でる/湯煎するという処理は時間を消費しました。

では「あるサイズのデータ」があるとき、それをユーザーの端末に書き込む時間は、予測可能でしょうか? また、サーバーからユーザーのアイコン画像を取得するとき、取得するためにかかる時間は、どのユーザーでも一定でしょうか?

非同期処理で難しいのは「ある非同期処理が終了したときに、どのように次の処理を始めるか」です。 これをオードリー)で説明してみましょう。

漫才は、基本的に同期処理で話が進んでいきます。 すごい簡単に書けばAネタ→Aボケ→Aツッコミ→Bネタ→Bボケ→Bツッコミ……です。 これはボケとツッコミが逆転する*5ことはあれど、話題は常に順々です。

オードリーの春日氏は、非同期処理の使い手です。 振られたネタをストックし、任意の時間を使いボケとして返します。 *6 春日氏が返してくるボケは、若林氏が振った話題の順番に依存しません。 このため、若林氏は春日氏から返される「ボケの内容」から「話題」を引っ張り出し、適切なツッコミをすることになります。 用意していたネタが全て終わったとき、もしくは番組の都合で終了時間を迎えたとき、漫才は終了します。

これはプログラミングにおける非同期処理の管理と同じです。 非同期処理は「いつ終わるかわからない」ため、「終わった時に対応」するように あらかじめ 用意しておくことになります。 そして全ての非同期処理が終わったとき、もしくは外部的な事情で処理を打ち切る必要が生じたとき、一連の処理を終了することになります。 そうして「非同期処理を使って実現したい工程」の終了を管理できるようになります。

非同期処理はどれだけ時間がかかるかわかりませんが、その終了タイミングをキャッチできれば、プログラムを書くことができます。 オードリーが登壇している間は非同期処理が飛び交っていても、オードリーの時間が終われば、次の芸人が同期処理で構成されるネタをやってもいいのです。 同様に、同期処理の中で非同期処理が入っていても、「終了後に」同期処理を始めることで、全てを同期処理で書いていたのと(あまり)変わらない形でコードを書くことができます。


非同期処理にちょっとだけ親しみが持てたでしょうか? 今回はここまで、次回はコードを書く予定です。

*1:HPの回復処理を作る時、毎回ユーザーの状態を確認したり最大値をチェックするより「HPを回復するメソッド」を用意し、それを呼び出すだけにしておいたほうが楽ですよね?

*2:次の段落からは、マイクロソフトのドキュメントを読んで把握できる場合は読まなくても大丈夫です

*3:厳密には「調理」フローの中にパスタ工程とソース工程があり、順々に実行するか並列に実行するか、と表現したほうがよさそうです。

*4:筆者は初めてカレーを作ったとき、炊飯とカレールーの用意を同時に行うのがとても大変でした。

*5:これも平行と並列の話で使えそうなメタファーですね

*6:なんのことかよくわからない方は、なんらかの合法的な手段でネタを1本見てみてください。