2016-09-20

LabVIEW のシンプルな並行処理制御

LabVIEW で、複数の並行処理を開始/停止させるアプリケーションを開発する機会があったので、メモ。

このアプリケーションは、画面を操作するとデータの集録を開始したり終了したりする。あるいは、データ集録開始後、一定時間が経過すると、自動的に集録を終了する。こういうのは、Producer/Consumer パターンで実装するのが、LabVIEW では定石みたいになっていると思う。


( image from http://www.ni.com/white-paper/3023/en/ )

ところがサンプリングレートやタイミングが異なるような、複数のデバイスからデータを集録するようなときは、ちょっとだるい。

たとえば


  • アナログ電圧のデバイスから、サンプリングレート 10k/s で 0.1秒ごとのかたまりで取得
  • シリアルポートに繋がった電流計から、0.7秒ごとに電圧を取得
  • CAN ポートにメッセージが到達するごとに集録(いつ届くかは分からない。届かないこともある)
  • デジタル信号のデバイスから(以下略)


というのを並行処理すると考える。ループのタイミングが違うので、アナログ、シリアル、CAN で、それぞれ別のループを作ることになるであろう。すると、各ループは、事実上、別スレッドで動くアクターになる。

やりすぎ感


NI のサンプルとか、フォーラムを見てると、ここも Producer/Consumer として実装している感じがする。だけど、ちょっと、だるい。

Consumer ループというのは、実装としてはイベントストラクチャを内包するループなんだけど、ロジック上はステートマシンになっている。でも集録アクターにおいて、複雑な状態遷移は起こらない。

デバイスの初期化 → 集録開始 → データ取得 → データ取得 ... → 集録停止

データ集録を繰り返す以外は、一直線に遷移するだけなのに、ステートマシンとは大げさな。初期化して開始して、ずーっと集録しつづけて、外部からトリガがかかったときに停止すればよい。

途中でエラーが出たときに、呼び出し側が丁寧に対処することは、ほとんどない。複数の信号ソースを同期しながら集録するようなアプリケーションで、どこかひとつのチャンネルでエラーが出たら、その回のデータは使えないのだ。

アクターフレームワーク


あと、アクターフレームワークというパレットに、いくつかVIがある。もともとサードパーティのものをカジュアルに同梱しただけらしく、ドキュメントがない。クラスを使っているんだけど、どのメソッドが必須なのかとかも書かれていない。ソースを読んだところ、ちょっと大げさな感じであった。

というわけで、もうちょいシンプルに書いた/描いた。

Actor VI


各デバイスのデータ集録のプログラムを actor.vi と呼ぼう。



上半分は同期とか、並行処理とか考えず、単純に電圧を集録するプログラムになっている。サンプルプログラムほとんどそのままだ。

下半分にキューがあって、終了するかどうかを決定する。終了する条件は以下のいずれか。


  • なんかエラーが出た
  • 参照しているキューが破棄された
  • タイムアウトなしで、デキューできた (=停止メッセージを受け取った)


ループを脱け出したら、デバイスの片付けをして終了する。

Main VI


呼び出し側は、以下のような感じ。



まずキューを取得し、ここではサンプリングレートとともに、Actor VI に渡す。もっとたくさん設定項目があれば、それも渡せば良い。そして Actor VI を実行する。

ここでは0.1秒の遅延をさせているけれど、実際にはユーザーインターフェースの停止ボタン待ちだったりする。

Actor VI を止めたくなったら、エンキューする。すると、Actor ではデキューに成功して、ループを抜け出して、終了することになる。

キューの管理


このサンプルでは、呼び出し側がエンキューの直後に、キューを開放しているんだけど、実際のアプリケーションでは、そんなにすぐに開放しない。ユーザー操作待ちの時間があったり、もういちど Actor VI を動かす処理が入ったりする。

キューの取得と開放は、Actor 側でもできるんだけど、あえてやっていない。一旦取得したキューを開放せずに VI が終了することを繰り返すと、毎回数バイトがガーベッジコレクターに回収されずにたまっていく。開発の途中で、メモリが増えていくことに気付いて困ったので、「気をつけて開放するようにコードを書く」のではなくて、「開放のことをほとんど考えなくていいコードにする」ということにした。

Go~


呼び出し側から、停止命令を伝えるための経路を渡し、呼び出され側は停止命令が来るまで動くっていうのは、Go のコードで何度か見かけた。チャンネルを渡すやつですね。Go に限らず、アクターにメッセージ渡す系の書き方だとこうなるのであろう。

先月リリースされた LabVIEW 2016 には、並行処理ループ間でのメッセージ渡しを、簡単に記述できる、まさに「チャンネルワイヤ」というのが入ったらしい。ほほう、遂に。

2016-08-15

LabVIEW における VI の実行状態

LabVIEW でプログラムを作っている仮定で、VI の実行状態に関して困ったり調べたりしたのでメモしておく。

VI というのは、大雑把に言うとユーザー定義の関数である。名前空間的なものを含めて、すべての VI は必ずユニークな名前がある。 GetVoltage.vi とか。

で、プログラム実行中に、VI名を指定して、そのVIの実行状態を知ることができる。状態は、4つで、 「Bad」「Idle」「Run on Top Level」「Running」だ。

Bad は、実行できない状態である。通常、開発システムで編集なので、メモリ上に載っているけれど、コンパイルが通らない状態だ。

Idle は、コンパイルが通るが、実行されていない。

開発ツールのVIウィンドウの実行ボタンをクリックして実行したり、実行形式にビルドしてあって最初に実行されるVIである場合、Run on Top Level という状態になる。main() 関数が最初に動いているみたいな状態だ。

で、Running という名前が直感に反する。VI は、任意の VI の中に配置すると、サブルーチン呼び出してきに呼び出せる。ユーザー定義の関数だから、当然、そういう使い方をする。仮に、Main.vi の中に、Sub.vi を配置しているとしよう。

Main.vi を実行すると、Main.viは「Run on Top Level」状態になる。このとき、Sub.vi の処理自体が実行されている状態でなくても「Running」になる。Main.vi によってメモリ上にロードされて、Main.vi から実行依頼(正確には、メッセージがパス)されているのを待っているときでも、Running になる。

だが罠がある。

デフォルトでは VI は再入可能ではない。Main.vi が Sub.vi を呼び出している間は、他のところから Sub.vi を呼び出せない。実行中の処理が終了するまで、メッセージを渡すのを待つ。

けれど、オプションで再入可能にできる。そしてこの場合は、呼び出しごとにメモリ領域が確保され、「Sub.vi:1」「Sub:vi:2」という風にVI名が識別される。ところが、メモリー上に Sub.vi:1 とかのリストを取得する手段がない。どういうことだよ。なので動いてたら殺すみたいなのをするのに、やたら苦労する。


2016-08-09

久しぶりに LabVIEW で並行処理コードをかく

久しぶりに LabVIEW を使って、コードを書いている。UIフレームワーク+信号処理ライブラリ+ビジュアルプログラミング言語のIDE というところでしょうか。

National Instruments 製のハードウェア(電圧入力とか、CAN入出力とか、カメラによる画像入出力とかのデバイス)を使うなら、ドライバーとUIで全力で手抜きできる。Init、Start、Read、Stop するためのアイコンを置いて、Read の戻り値をUIコンポーネントに接続するだけでいい。

あと、実行単位が静的に決まっているときの並行処理コードも楽ちんである。



基本的に、左のほうのアイコンから、右のほうのアイコンに、データが伝搬されていく。四角く囲んであるのは、Whileループで、条件が成立するまで繰り返し実行される。

で、上と下の While ループどうしは依存関係がないので、なんとなく並行処理するようになる。両方のループが完了すると、右のほうにワイヤーが集約されたところで join されてめでたく終了する。便利だ。

便利なんだけど、あまりに久しぶりなので、細かいことを忘れていたり、他のプログラミング言語でできたことをうまくLabVIEWの考え方にマッピングできなくて、時間がかかっている。知り合いのトイプー使いが「プログラミングはスポーツみたいなもんで、すっと体が動くようになるといいね」みたいなことを言っていて、まさにそのとおりだと感じている。