【LabVIEWまずこれ㉒】並列処理と変数について

LabVIEWまずこれ

スポンサーリンク

LabVIEWを触ったことがない方に向けて、それなりのプログラムが書けるようになるところまで基本的な事柄を解説していこうという試みです。

シリーズ22回目としてLabVIEWの強みである並列処理と変数を扱っています。

この記事は、以下のような方に向けて書いています。

  • 複数の処理を同時に行うにはどうすればいいの?
  • 複数の箇所で同じ値を共有することはできるのか?
  • VIをまたいで値を共有したい

もし上記のことに興味があるよ、という方には参考にして頂けるかもしれません。

なお、前回の記事はこちらです。

スポンサーリンク

並列処理とは

ステートマシンが十分理解出来たらもう初心者ではなく立派なプログラマーになりつつあるのですが、それでもまだまだ紹介しきれていない機能、組み方が多くあります。

デザインパターンもステートマシンは確かに強力なのですが、これだけだと実はLabVIEWの強みを活かすことができません。

LabVIEWの強みの一つであり、かつ今まで明確には触れてこなかった特性、それが「並列処理」です。

そしてこの並列処理を扱ったデザインパターンで強力なものがあるのでそれらを理解するためにももう少しいくつかのトピックについて扱っていこうと思います。

実はこれまでの話では(ほとんど)処理は「ひとつなぎ」でした。「何か処理を行ってその結果を次の関数に渡して・・・」ということを繰り返すというわけです。

ただ、一部中にはそのような順番が定まっていないようなものがありましたね。例えば、こんな簡単なプログラムのようなものです。

LabVIEWではある関数への入力が全てそろってからその関数が実行されるという絶対的なルールがありました。そしてこのルールにより関数の実行の順番が決定されます。

一方でこのプログラムについては、乱数を表示させる部分と待機関数は「独立」しています。片方からもう片方へ値を渡すという状態ではありません。どちらかが先に実行されてそれが終わらないともう片方が進まない、という書き方になっていないので前後関係が不明です。

実はこのような書き方によって「並列処理」をしていることになっています。

つまり

  • 乱数の値が表示される
  • 待機関数が100ミリ秒待つ

という動作が並行して行われている、ということです。(ただ実際は待機関数を他の処理の関数と同等にみなして並列処理をしていると意識することはほとんどないと思いますが)

まぁこんな例で並列といってもあまりありがたみがないと思うので、もう少し別の例で見てみます。

このような場合、Whileループが二つ置かれていてこれらにはやはり前後関係がありません。それぞれのWhileループ全体をひと固まりで関数のように見立てればわかりやすいかもしれません。とにかく、どちらかが完了しないと次のWhileループが始まらない、という前後関係がないのです。

他の言語に慣れている方にとっては並列処理が結構苦労されるポイントのようなのですが、LabVIEWは比較的簡単に並列処理を実装できます。単純な話で、「順番を決めなければそれで並列になる」ため、直感的に複数の処理をおくだけで並列処理とすることができます。

もちろん例えば3つのWhileループがあれば、それらも並列で動作させることができます。これらのWhileループがブロックダイアグラム上でどこに置かれているか(あるWhileループに対して他のWhileループが上に置かれているのか左に置かれているのかなど)は関係ありません。

ただ一方で、並列処理が簡単にできるということで問題も生じます。

例えば、上記の例ではWhileループごとに停止ボタンを用意していました。つまり、フロントパネルにはWhileループの数だけ停止ボタンがあり、プログラム全体を止めるためにはそれらを全て押す必要がありました。

では、一つのボタンを押すことでWhileループを全て止めたいと考えた場合どうすればいいか、という話になります。

シンプルな例として、二つのWhileループを扱う場合を考えます。例えば下の図のようなプログラムを書いた場合にうまく意図したとおりにプログラムが動くのか?

実行してみるとわかるのですが、これは意図したとおりに動作しません。右のWhileループがプログラム開始時には動作しないからです。乱数表示と書かれた数値表示器の値が更新されないことからわかります。

なぜなら、この書き方では二つのWhileループに明確に実行順番が決められているからです。左側のWhileループ(ループ1)はプログラムが実行されるとすぐに実行されます。一方で右側(ループ2)は始まるためにはループ1から出力されたブールの値(停止ボタンの値)が渡らないと、ループ自体に「すべての入力がそろった」状態にならないからです。

しつこく繰り返しますが、LabVIEWプログラムの順番決めの鉄則で、すべての入力がそろわないと処理が開始されないというものがありました。主に関数に対して表現したりしますが、Whileループという構造自体も入力トンネルにすべての値がそろわないと開始しないのは同じことです。

となると、二つのWhileループを一つの停止ボタンでどちらも停止させるという書き方はできないということになる・・・いやいやそんなことはありません。とはいっても今までのプログラムの手法ではこれを解決することができません。

そこで、新たなトピック、ローカル変数の登場です。

ローカル変数

ローカル変数とは、「ローカル(同じVI内)で使用できる変数」のことです。

どういったものなのか、プログラムを見ればすぐに理解できるものだと思うのでまずは使用例を示します。

上の図のループ2で使用されているものがローカル変数です。ループ1の停止ボタンと同じ名前、同じ色をしています。

これはいわばループ1の停止ボタンの化身であり、ループ1の停止ボタンと同じ値を持ちます。そのため、もしフロントパネル上の停止ボタンが押されるとループ1が止まるばかりではなく、これと同じ値を持つループ2も止まります。(もしこの説明の時点で違和感を持った方はかなりセンスがあると思います。その違和感については後述します)

このローカル変数は、同じ名前(ラベル)の制御器や表示器の値と同じ値を持っています。なおかつ、このローカル変数へ値を書き込むことで、もともとの制御器や表示器、そしてさらに別のローカル変数にその変更を反映させることができます。

ブール以外のデータタイプでも示すと以下の図のようになります。

ローカル変数は一つの制御器/表示器に対していくらでも作ることができます。この性質を利用して、もっと複数の並列Whileループを止めることもできます。

ではローカル変数を実際に作ってみます。単純に作り方だけさえわかればあとはどのような場合でも同じように操作するだけなので、余計な内容は排除して、ただのWhileループを二つ用意し、片方にだけ停止ボタンを用意します。

当然、停止ボタンがついていないWhileループは停止条件が定まっていないので、プログラムはこのままでは実行不可です。

ローカル変数を作る対象、今回で言えばWhileループの条件端子につなげている停止ボタンを右クリックします。そして作成からローカル変数を選びます。作成されたローカル変数はデフォルトでは書き込みになっているので、右クリックして読み取りに変更することでWhileループの条件端子に繋げられるようになります。

では実際にローカル変数を右側のWhileループの条件端子に繋いでみます。これで完成、と思いきや、プログラムの実行ボタンが依然として壊れていることがあります。

これはブールデータタイプのローカル変数で起こりがちなことで「機械的動作がラッチの場合にはローカル変数を作れない」という制約があります。上で「違和感を持った」方のその違和感の正体はこの制約が関係しています。

これは、ラッチの性質を考えればわかります。ラッチはLabVIEWプログラム自身がTRUEを検出したらそれをFALSEに戻すという役割をしていました。

ローカル変数はもともとの端子の化身であり同じ性質を持っているため、もともとの端子がラッチ動作だった場合にはローカル変数もラッチ動作になるはずです。となると、プログラム中に複数ラッチが散らばっていた場合、LabVIEWはどれを実行した際にTRUEをFALSEに戻すかが非常にあいまいになります。さらに、ローカル変数はプログラム的に値を書き込むこともできるのでさらにややこしくなります。

元々の端子(ローカル変数ではないもの)が優先される、といった決まりもありません。そのためブールはラッチ動作ではローカル変数の使用が禁じられています。

一方で、機械的動作がスイッチだと、基本的にこれはユーザー、あるいはプログラム的に切り替えない限りTRUEとFALSEが入れ替わりません。「(LabVIEWにより)勝手に」切り替わることがない動作なのでローカル変数として使用できます。

そのため、プログラムが壊れている原因であるブールの機械的動作をスイッチに変えることでプログラムが動くようになります。

ただ、この際に今度は別の問題が生じてきます。それは、スイッチの動作になったことでWhileループが終了してもブールはTRUEのままになっているということです。

この問題は、Whileループが終わった後にプログラム的にスイッチのローカル変数にFALSEを書き込むことで解消できます。

ブールについては上記の機械的動作という特殊な事情があるのでローカル変数を作成したところでエラーが起こることがありますが、他のデータタイプでは特にそういった制約なく自由に何個もローカル変数を作成、使用することができます。

グローバル変数

変数にわざわざ「ローカル」変数という名前がついているくらいなので他にも変数と呼ばれるものはあります。

ローカル変数は同じVI内でデータを受け渡ししているものでしたが、異なるVI同士でもデータを受け渡しできるものがあり、これはグローバル変数と呼んでいます。

作り方としては、グローバル変数の枠をまず用意します。そしてこれをダブルクリックで開きます。すると「フロントパネルしかないvi」が開くので、ここに複数のvi間で共有したいデータタイプを配置していきます。注意点としては、データは複数置ける、という点です。

作成したグローバル変数は、元々のvi上に置いていたグローバル変数の端子からであればそのままグローバル変数のフロントパネルに置いた制御器を選択することができます。

あるいは関数パレットの「VIを選択」で作成したグローバル変数(vi扱いになっています)を選択することでもブロックダイアグラムに配置することができます。

vi間でデータの受け渡しができるので、例えばメインのプログラムからサブVIで扱われているデータを見ることができます。

ローカル変数とグローバル変数は避けるべき

ここまでローカル変数やグローバル変数の使い方、利点を紹介し、これは便利だと思われた方。ここまで説明しておいて何ですが、実はこれらの変数は基本的に使用を推奨されていないようです。

そのため、機能としては存在するものの、基本的に使用しないようにするべきです。

推奨されていない理由は思いつく限り以下のような理由が挙げられます。

  • 競合状態が発生する
  • ワイヤを介していないのでデータの流れがわかりにくい
  • 必要以上にメモリを消費する
  • ワイヤでのデータ転送より遅い(ヘルプに記載「ローカル変数を使用する場合のメモリに関する注意事項」)
  • 最新値しか共有しない
  • 前の実行時の値が残っている

まずは最大のデメリットと言っても過言ではない、競合状態です。LabVIEWの並列処理が裏目に出る例とも言えます。

例えば以下のようなプログラムを考えたとします。

このプログラム、フロントパネル上に現れる値は常に同じとは限りません。なぜなら、同じ表示器に対して3つの箇所で異なる値を入力しており、それらの実行順番が決まっていない、つまり並列で動いているためです。これを回避するには値の書き込みの順番を明確に定める必要があります。

「こんなプログラム書かないよ」と思われるかもしれません。確かに上のプログラムは例として紹介しているもので実用的ではないものですが、結果的にこれと似たプログラムとしてしまう可能性は大いにあります。

なぜそんなプログラムを書いてしまうことが起こるか、というのが次のデメリットに関係してきます。それは、ローカル変数は単体として存在することからLabVIEWで特徴的な「ワイヤでデータを渡していく」というルールから外れた存在だということです。これは、ひとえにプログラムを読みにくくすることにつながります。

そのため、ある程度大きなプログラムだと、知らず知らずのうちに競合状態を起こしてしまう様な書き方になってしまうことが大いにあり得るということです。

ただし厄介なのは、競合状態になっていたとしても「多くの場合この結果になる」ということが決まっているように見えます。何百、何千回実行すると稀に値が異なる、みたいなことになるので十分注意する必要があります。

実はローカル変数を使わずにローカル変数でやろうとしていることを実現する書き方があります(次回扱います)。ただしそういった書き方と比べるとローカル変数の方が簡単に実装でき、読みにくいとはいえ見た目はシンプルなので、「必要悪」として使用する程度にします。

また、メモリを消費するというのもデメリットになります。要はデータの複製を作っているわけなので、ローカル変数それぞれに対しメモリがあてがわれるので大規模プログラムを作成する際にメモリ不足に陥りかねません。

そして、メモリだけではなく実行速度のパフォーマンス面でもローカル変数は遅くなるようです。そのためパフォーマンスを気にする必要があるプログラムの場合だとローカル変数ではなく他の方法を使用する必要があります。

また、最新値しか共有しないということもデメリットになりえます。例えば、ある端子にデータが書き込まれたとして、そのデータが別の場所にあるその端子に対するローカル変数で読み取られる前に、元の端子に新たな値が書き込まれたとき、ローカル変数は最初に書き込まれたデータを読み取ることができなくなります。これはループ間でデータを共有する場合に問題となりえます。

一応の対応方法としてループ間でタイミングを同期させるために「次のミリ秒倍数まで待機」を使用することで大方うまくいきますが、完璧ではありません。

さらに、前の実行時の値が残っていることがネックになる場合もあります。LabVIEWを終了しない限り、ローカル変数には値が残り続けているので、例えばプログラムの最初にローカル変数の値を読み取るような操作があった場合には期待通りの結果が得られない可能性があります。

ここまでの話で、ローカル変数ではなく他の方法を使おうと説明してきました。「元々ループ間でデータを受け渡すために使うという話だったが他の方法もあるのか?」ということで登場するのが「キュー」という関数です。

このキュー関数、使い方を少しだけ覚える必要があるのですが、その分強力なプログラム手法になっているので並列処理という特性をさらに活かすための知識としてぜひ持っておいた方がいいものになります。

ということで次回はこのキュー関数の扱いについて紹介していきます。

もしよろしければ次の記事も見ていってもらえると嬉しいです。

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

コメント

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