ループのタイミングのお話

ステップアップ

スポンサーリンク

この記事は、初心者向けのまずこれシリーズ第10回の補足記事です。繰り返しの処理に大切なタイミング関数をもっと知りたい、という方向けにもう少し内容を補足しています。

LabVIEWでプログラムをある程度作れるようになったら、一見どういう違いがあるのかわからない関数についてもしっかり使い分けないといけなくなる場面があると思います。

その代表が「待機」関数と「次のミリ秒倍数まで待機」関数かなと思います。(「次のミリ秒倍数まで待機」は長いので、略して「次ミリ」と呼ぶことにします)

この二つの関数の区別がつかない!という方に読んでいただけると参考になるかなと思います。

スポンサーリンク

ループ制御の関数

待機関数と次ミリ関数の二つは、どちらもループのタイミングの制御を行うために使用します。

ただし、使い分けをしないと、場合によっては「意図しない」結果になることもあるので注意する必要があります。一言で表すと、

  • 待機:指定した値の時間分、絶対に待機する関数
  • 次ミリ:コンピュータのシステムクロックが指定した値の倍数になるまで実行が終わらない関数

です。これではまだ意味が分からないと思うので、以下解説していきます。

待機関数

待機関数はクセがなくわかりやすいと思います。その名の通り、指定された時間の分だけ「待機する」関数です。

下のようなプログラムを例にとってみます。

ループの復習です。WhileループでもForループでも、ループというのはその枠組みの中に書かれた処理や関数が全て実行されないと次のループに進まない、というものでした。

上の図の例の場合には

  • 乱数から値を出して表示させる、という処理
  • 待機関数によって100ミリ秒待つ、という処理

の二つがあります。Whileループの毎ループごとに、この二つの処理が同時にスタートすると考えてください。

乱数から値を出して表示、という処理は一瞬で終わりますね。これに対して、待機関数は上の図の例の場合100という値が配線されているので、100ミリ秒待つ、という処理を行います。

では、このループ1回が終わるのは、始まってから何ミリ秒後でしょうか?

そう、100ミリ秒後ですね。なぜなら「ループというのはその枠組みの中に書かれた処理や関数が全て実行されないと次のループに進まない」ので、遅く終わる処理にかかる時間が経過するまで次に進まないからです。

待機関数がなければループは全力で(CPUがフル稼働して)回りますが、待機関数があることによって強制的に100ミリ秒足を引っ張ります。これが、待機関数の役割です。

敢えて時間軸と表すなら以下のような図式です(矢印の縮尺はめちゃくちゃですが)。

大げさに、仮に乱数から値を出して表示させる処理に1ミリ秒かかっていたとすると(もちろんこんなに時間はかかっていないですが)、残りの99ミリ秒は、このWhileループは文字通り「待機」している状態です。もしプログラムにこのWhileループしかなかったら、LabVIEW自体が休憩中となります。

すると、PCとしてはLabVIEWを動かす必要がないので、PCのCPUはLabVIEW以外のアプリケーションを実行する余裕が生まれます。必要以上に早くループさせないことで、アプリケーションそのものの要求としてタイミングを制御するだけでなく、PCへの負荷および他のアプリケーションへの影響を減らす役割も果たします。

では、次に下の図のような例を考えてみます。

上の図の例では、サブVIがあって、このサブVIはWhileループの中の待機関数が待つ時間、100ミリ秒よりも多い、120ミリ秒かかってしまう処理としています。

ではこのとき、一回のループにかかる時間はいくらになるでしょうか?100ミリ秒・・・ではないですよね?もちろん、120ミリ秒です。

なぜなら、待機関数が100ミリ秒待ち終えても、サブVIの方はあと20ミリ秒経たないと処理が終わらないからです。ここでも「ループというのはその枠組みの中に書かれた処理や関数が全て実行されないと次のループに進まない」というルールが効いてきます。

「では待機関数の意味がないんじゃないの?」その通り、意味がなくなってしまいます。そのため、待機関数を使用する際には「待機関数以外の処理で一番時間がかかる処理よりもさらに長く待つように待機関数への入力の値を決める」ことが重要です。

つまり、先ほどの例では100を待機関数に配線するのではなく、120よりも大きい値を入れることで待機関数の意味が出てきます。

ではそもそも処理にかかる時間なんてどうやって測ればいいのか?、ですが、簡単な測り方は次のようなプログラムが便利です。

ここで使われているティックカウントとは、PCのシステムクロックの値を見ています。なんじゃそりゃ?という方は、とりあえず、1ミリ秒の精度である開始時間からの経過時間を見ているストップウォッチだと思ってください。

このストップウォッチならぬティックカウントを二つ使用して、その間に時間を測りたいコードを置きます。で、前後で取得したティックカウントの値の差をとると、真ん中のコードの時間が測れる、というわけです。

あるいは、かかる時間がまちまちになるかもしれないコードについては、こんな方法も有効かもしれません。

上の図で示した上下二つのコードはどちらでも構いません、やっていることは同じです。

この方法だと、Forループで指定した回数実行をするんですが、その前後のティックカウントの値の差をとってから、ループの回数で割ることで、一回当たりの平均を出すことができます。これで、おおよそかかる時間の平均値が分かるようになりますね。

このようなコードを書いてループの中にある待機関数以外の処理にかかる時間を調べ、それらの中で一番長く時間がかかるものよりもさらに長い時間を待機関数に配線するようにします。

次のミリ秒倍数まで待機

次に、次ミリ関数の話です。この関数は、ティックカウントの説明でも登場した、「PCのシステムクロック」が、次ミリ関数で指定した値の倍数値になるまで待機する関数になっています。

例えば次ミリ関数に100と配線したとします。すると、次ミリ関数はシステムクロックの値を見て、その値が100の倍数(200、とか、300、とか10300とか)になるまで待機します。

こちらも例として次のようなループを考えてみるとします。

次のループに進むまでのルールは、待機関数の時にお話ししたものと変わりません。「ループというのはその枠組みの中に書かれた処理や関数が全て実行されないと次のループに進まない」、です。

なので、このループの場合には、

  • 乱数から値を出して表示させる、という処理
  • 次ミリ関数が指定された時間待機する、という処理

これら二つが同時に実行されると考えます。

このループを実行するとき、最初のループが終わって次のループに進むまでの時間はいくらでしょうか?100ミリ秒・・・ではありません。

答えは「毎回異なる」です。え、じゃあ次ミリの関数に渡している100という数値はどうなるの?と疑問に思うかなと思います。この理由は「そもそもループ開始時のシステムクロックの値はわからない」から、です。

例えば、ループを始めた瞬間、つまり次ミリ関数がシステムクロックの値を見たときに、システムクロックが123456という値だったとします。

この123456という値は100の倍数ではないですよね。システムクロックの数値は増加し続けるということを考えると、123456から数えて、次に100の倍数になるのは123500です。つまり、44ミリ秒後、ということになります。

そのため、もしこのループを始めて次ミリ関数がシステムクロックの値を見たときの値が123456なら、次ミリ関数は「44ミリ秒待機する」関数となります。

あれあれ、100と指定したのに44ミリ秒しか待機しないのか・・・と思われるかもしれませんが、めげずに2回目のループを考えてみます。2回目のループから3回目のループに進むのは何ミリ秒後でしょうか?

正解は・・・100ミリ秒後です。44ミリ秒後ではありませんよ。なぜなら、2回目のループが始まったときに次ミリ関数がシステムクロックの値を見たときには、123500になっているからです。これより大きくて100の倍数の時間は123600ですね?なのでこのときには100ミリ秒待つという状態になります。

以降は同じ、100ミリ秒ずつ待ち続けます。よって、次ミリ関数は、最初の一回以外は指定した時間分待つということになります。

では応用編。ちょっと天邪鬼な例ですが、こんなプログラムも考えてみます。

次ミリ関数以外に置かれたサブVIは、通常80ミリ秒程度で終わるのですが、たまに140ミリ秒かかってしまうとします。こんなループがあるとき、各ループでどれくらい時間がかかるかを考えます。

ここでは、システムクロックの値が5678であったときにループが開始したとします。

1回目のループ。サブVIは通常動作をしたとします。すると、サブVIの実行には80ミリ秒かかります。一方で、次ミリ関数はどれだけ時間がかかるでしょうか?5678より大きくて一番近い100の倍数は5700ですね?ということで22ミリ秒待機するという動作をします。

よってこの場合には、1回目のループ開始時から数えて2回目のループが始まるまでの時間は80ミリ秒後、ということになりますね。

2回目のループ。またもやサブVIは正常動作をしたとします。よってサブVIの実行は80ミリ秒。次ミリ関数は5700から始めて、次の100の倍数である5800まで待つので、100ミリ秒です。

ということは・・・2回目のループ開始時から数えて3回目のループが始まるまでの時間は100ミリ秒後、ですね。

3回目のループ。ここで、サブVIは通常通りの処理ではなくなり、140ミリ秒かかったとします。一方で、次ミリ関数は5800から5900までということで100ミリ秒待ちます。

となると、3回目のループ開始時から数えて4回目のループが始まるまでの時間は140ミリ秒後になりました。

4回目のループ。サブVIは正常に動作して80ミリ秒となりました。では次ミリ関数はどうか?5900から・・・ではありませんよ。3回目のループで140ミリ秒経過してしまっているので、システムクロックは40ミリ秒オーバーして、5940から数え始めです。

すると、次の100の倍数が6000なので、60ミリ秒待ちます。となると、サブVIの方が時間がかかっているので、4回目のループ開始時から数えて5回目のループが始まるまでの時間は80ミリ秒後になります。(以下の図は特に縮尺がむちゃくちゃになっていますのでお気を付けください)

5回目のループ。サブVIは正常に動作して80ミリ秒とします。次ミリは6020から6100で80ミリ秒、よって5回目のループ開始時から数えて6回目のループが始まるまでの時間は80ミリ秒後です(たまたまサブVIと同じだけの時間となりました)。

以降はループ2回目と同じで、サブVIが80 ミリ秒で終わったとしても次ミリ関数が6200までの100ミリ秒待つことになります。このような考え方を繰り返して、ループにかかる時間を制御していきます。

待機関数と比べて素直ではない挙動をすることもありますが、これはこれで利点があることを以下で説明していきます。

ところで、「最初のループのときにも指定した時間待機してほしいけれどできないの?」と思われるかもしれません。そんな場合には、わざとループの前に一つ次ミリ関数を置いて「補正」をしておけばいいんです。

どの次ミリ関数も参照しているシステムクロックは共通しているので、ループ前にタイミングを「補正」しておけば、ループに入ってから初回の場合もほぼ指定した時間待機させることができます。

二つのタイミング関数の使い分け

ここまでの話を聞くと、「次のミリ秒倍数まで待機は指定した時間分待たない可能性があるってこと?使いにくい!」と思うかもしれません。でも、これはこれでちゃんとメリットがあるんです。

それは、複数のループに対し、各ループの始まりのタイミングをそろえることができる、という特性です。

複数のループに対して次ミリ関数が使用されている場合、各ループの中の次ミリ関数はどれも、同じ時間の基準(システムクロック)を基にどの程度待つかを決めています。そのため、複数ループがあった場合に、それらのタイミング制御を同期させて行うことができます。

あるいは別の言い方をすると、複数のループをどれも一定タイミングで動かす場合に次ミリ関数を使用していると、例えばそのうちの一つのループの処理が何かしらの原因で遅れた場合であっても、遅れを補正して何ループ後かあとに再び同期をとることができるようになります。

先ほど次ミリ関数の流れを確認したときのシナリオを例に、サブVIで遅延が起こったとき(ループ1)と起こらなかった(ループ2)ときで比べてみると、遅延が起こったとしても再び同じタイミングになることがわかります。

この点、待機関数は一度遅れたとしても次のループではまた律儀に(?)指定したミリ秒だけ待機を行います。つまり、ずれが生じた場合にはその「補正」は行われません。待機関数は複数のループで使用されていても、それぞれ独立して動作するからです。

比較用に、次ミリ関数に対応して同じサブVIを使用して考えたとすると以下のような図の流れとなります。

ずれたらずれっぱなしでループを回し続けている様子が分かるかと思います。

この点を考えると次ミリ関数はソフトウェア的に複数のループの同期を行うためには便利ではあるのですが、ソフトウェアの動作を行っているOSの精度でしか同期ができないことに注意します。Windows OSでは、ミリ秒単位の精度しか持たないことは覚えておいて損はないと思います。

ループを使用する際にタイミングを制御する関数はよくセットで使用されます。そこで出てくる、待機関数と次のミリ秒倍数まで待機関数の動作の違い、および使いどころの違いはわかりましたでしょうか?最後にもう一度おさらいしておくと、

  • 待機:指定した値の時間分、絶対に待機する関数
  • 次ミリ:コンピュータのシステムクロックが指定した値の倍数になるまで実行が終わらない関数

でした。これらの違いと、「ループというのはその枠組みの中に書かれた処理や関数が全て実行されないと次のループに進まない」というルールを念頭に入れておけば、LabVIEWでループを扱う際の迷いも少なくなると思います。

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

コメント

タイトルとURLをコピーしました