質問者
スレッドと割り込みで共用している変数の排他制御

質問
-
スレッドと割り込みの間での排他制御についてご教示願います。
Visual Studio 2015で、C#を使用します。
スレッド間で共通の変数をアクセスする場合、排他制御をおこなうためにlock()を使用しますが、
スレッドと割り込みの場合でもlockは使用可能でしょうか。不可の場合、他の方法をご教示願います。
1) スレッドで定期的に更新しているデータを、割り込みハンドラ中で読み込んで使用する
2)割り込みハンドラで割り込み時に更新したデータを、スレッドで読み込んで使用する
の2種類を行いますが、1)と2)のデータは、別々のもので、
スレッドと割り込みの双方から同じデータに書き込むことはありません。
データ量は、1),2)のいずれも4word程度です。
スレッドと割り込み間でlock()で排他制御を行うと、デッドロックになる可能性がありそうです。
以上、よろしくお願いします。
すべての返信
-
I/Oボードメーカ提供のライブラリでC#が利用可能という前提で、I/O管理オブジェクトのイベントハンドラを登録しての利用と想定しますが、別スレッドで呼び出されると考えてまず間違いがないと思って良いと思います。
互いが書き込んだデータを読みあうのであれば、同じlockオブジェクトをlockすると、懸念されている通り、デッドロックが発生します。
通常は、データごとにlockオブジェクトを変えるのが最低限必要かと思います。
また、 4 wordということなので大丈夫と思いますが、lock中に時間のかかる処理やブロッキングコールを呼び出すことも厳禁です。
パルスカウンタの割り込みということですが、カウント毎に割り込みが発生するタイプですか?周期割り込みでカウンタを読み込むタイプのものですか?
どちらの場合も割り込み周期が短いと処理しきれないので、カウント毎に割り込みが発生する場合は対応できるカウントレートに制限が、周期割り込みの場合は割り込み周期に制限があり、応答性能や時間精度は期待してはならないと考えてください。
- 編集済み tmori3y2 2016年7月19日 9:04
-
ご回答、ご助言ありがとうございます。
C#から利用可能なボードメーカ提供ライブラリを使用します。
> 4 wordということなので大丈夫と思いますが、lock中に時間のかかる処理やブロッキングコールを呼び出すことも厳禁です。
lock中の処理は、メモリ上でのデータのコピー程度にする予定です。
> パルスカウンタの割り込みということですが、カウント毎に割り込みが発生するタイプですか?
> 周期割り込みでカウンタを読み込むタイプのものですか?
これは、前者です。
カウントごとに関数が呼び出されますが、この内容も最低限の処理にする予定です。
カウントのレートは最大500Hz程度なので、なんとか間に合うかな、とは思っていますが、
あまりうまくいかないようであれば、割り込みをやめて別スレッドを起動し、
このスレッド内でカウント値を監視し、1だけ増加したときに、割り込みで行う予定であった
処理を行うことも考えています。スレッドで監視する方がPCには負担になるとは思いますが・・・
応答性については、さほどシビアなアプリではなく、カウント値の増加を見逃さずに
予定している処理が毎回できればよいのですが。
-
いまいち質問内容を把握できていないのですが、呼び出しがパルスカウンタによって行われるだけということでしょうか? 「4word程度のデータ量」とありましたが、このデータは誰が書き込みを行い誰が呼び出しを行うのでしょうか? どちらもプログラムコード、つまり外部ハードウェアが読み書きを行うわけではない、ということでしょうか?
Yesの場合、さらに根本的な質問となりますが、4word程度というのは具体的に何byteなのでしょうか? …常識とお考えかもしれませんが、.NET用語ではありませんし、本来の用語定義であればプロセッサーの処理サイズでしたが、今時のプロセッサーは32bit / 64bitが当たり前となっており、それとはズレた値だったりしないでしょうか? 祖語のないよう正確な値を提示していただけたらと思います。
なぜこのようなことを書いているかというと、全ての条件を満たした場合、Interlockedクラスが使えるのではないかと考えています。この場合、lockによる排他が不要になります。
-
早速のご回答ありがとうございます。
また、質問があいまいで申し訳ありません。
ここでの割り込みとは、PCにIOボード(具体的にはパルスカウンタボード)を取り付け
そのボードから発生した割り込み時に呼び出される割り込みハンドラによる処理を示します。
よろしくお願いします。
例えば、http://www.contec.co.jp/product/device/counter/
ですかね。
(まだ着手していない場合も含めて)細かなプログラムは提示できなくても、使用しているボードや開発環境(できればAPIやラッパクラスの仕様)などを提示していただいた方がわかりやすいとは思います。
パルスカウンタですが、電気信号をカウントするハードウェアで、1チャンネルの矩形波のパルス数をカウントするものもありますが、通常は位相のズレた2相の矩形波をカウントするアップダウンカウンタが主流で、距離/回転角度、順方向/逆方向、速度などを計測するセンサなどで応用されています。
例えば、機械式マウスには、ボールの回転を2相の矩形波信号に変換するロータリーエンコーダというセンサが、XY方向に2つ付いていて、移動距離、移動方向、移動速度を画面上のポインタの動きに変換しています。
また、モータ制御では、回転数、回転方向、速度を計測するためのロータリーエンコーダをモータに取り付けて、カウンタ値の変化をトリガーとした割り込み処理で、読み取ったセンサ値をもとに、モータの制御パラメータを変更して、モータ制御コントローラに出力するフィードバック制御を行うことが多いですが、今回はそのようなフィードバック制御をやりたいのではないかと思われます。
「割り込み」とは、拡張ボードの「ハードウェア割り込み」を指し、通常であればデバイスドライバ経由でユーザ定義のC/C++の関数(割り込みハンドラ)が呼ばれますが、最近はC#でも利用可能なようにラッパクラスを提供しているベンダもあり、その場合は割り込みハンドラからC#のイベントハンドラが相互運用経由で呼び出されるのではないかと思います。
そういう意味では、今回のケースでは「割り込み」という表現は正確ではなく、「割り込み」をトリガーとして起動されるイベントハンドラというのが正しいと思われます。
センサデータがイベントハンドラの引数で渡されるのか、イベントハンドラで自分で拡張ボードのレジスタを読みに行くかは、ボードメーカの提供するライブラリのAPI次第です。
いずれにせよ、イベントハンドラではセンサデータをプログラム領域に保存し、別スレッドで何らかのアルゴリズムでリアルタイムに処理された制御データを拡張ボードにフィードバックするというのが今回の目標なのでしょう。
500Hz = 2msecですが、感覚的には厳しいように感じます。
* WindowsというOSは、組み込みシステムで使用されているようなRTOSではない
→ユーザプログラムでの時間精度・応答速度が保証されていない
* バックグラウンドで実行されているプロセス・スレッドが多いこと
→CPUは一般的な組み込みシステムより高速・高性能だが、ディスパッチが多く発生することによるオーバーヘッドがバカにならない
* イベントハンドラ自体、相互運用等を経由して呼び出されており、オーバーヘッドが大きい
* PCスペック(コア数/スレッド数/メモリ, etc)により、動作マージンが変動する
→あるPCでギリギリクリア出来ても、別のPCで上手くいくかも保証できない
時間精度が保証出来ないというのは、制御ムラが発生するということです。
モータ制御を例に取ると、回転ムラがあったり、急加速、急ブレーキなどが発生しうるということです。
比較的高精度のフィードバック制御でも、アルゴリズム次第では、100msec周期の処理で十分な場合もあるという点は指摘しておきたいと思います。
- 回答の候補に設定 星 睦美 2016年7月27日 2:58
-
tmori3様
たびたびのご助言ありがとうございます。
> 例えば、http://www.contec.co.jp/product/device/counter/
まさにこれで、
ボードは、CNT24-4(PCI)H で、ドライバはAPI-CNT(WDM) を使用する予定です。
今回は、制御ではなく、取得したデータの保存までです。
カウント値が増加するたびに、他のデータ(スレッド側でカウンタボード以外から取得しているデータ)とともに
HDDに保存しようと考えています。
割り込みで呼び出される関数にてHDDに保存するのは好ましくないと考え、
カウント値と、そのカウントアップ時のその他データをまとめて、スレッドの方に再度渡し、
スレッド側でHDDに保存しようと考えています。
-
佐祐理様
たびたびのご助言ありがとうございます。
排他制御に関連しているのは、同一のEXEファイルから使用される(1) 一つのスレッド
(2) 一つの関数のみとお考えください。
(2)の関数は、パルスカウンタボード付属のライブラリを使用して、パルスカウンタボードにハードウェア割り込みが発生した時に、
パルスカウンタボードのドライバによって呼び出されるようにドライバに登録します。
(2)の関数はパルスカウンタボードの割り込み時のみ呼び出されます。データの読み書きについては、
(1)のスレッドから(2)の関数には、publicで宣言した変数(4byteの整数×2個、4byteの浮動小数点数×2個)を通して
データを渡します。→これらの変数をまとめて(a)とします。((1)のスレッドで書いて、(2)の関数で読み取る。)(2)の関数から(1)のスレッドには、publicで宣言した変数(4byteの整数×3個、4byteの浮動小数点数×2個)を通して
データを渡します。→これらの変数をまとめて(b)とします。((2)の関数で書いて、(1)のスレッドで読み取る。)(a)と(b)の変数は異なるものです。
まだコーディングに入っておらず、プログラムの設計中のため確定ではありませんが、
変数の量は、大きくは変わらない予定です。浮動小数点の変数が4byteから8byteに変更になる可能性はあります。(b)の変数のうち一部は、パルスカウンタボードのカウント値ですが、ボードのドライバとライブラリを介して、
(2)の関数内でボードから取得したカウント値をメモリ上の変数(b)に書き込んでいます。
OSはWindows 7 32bitを使用します。
以上で、プログラムの動作はご理解いただけるでしょうか。
-
あまり詳しくないので想像でしかなくて申し訳ないのですが、こういうドライバからのC#やVBへのインターフェイスって、割り込みスレッドそのままではなくて、キューイングされてタスクみたいに順次呼び出しされてたりしないですかね?
※APIの仕様に詳細な情報はあると思いますけど
ドライバからの割り込みは、キューに情報だけ登録してすぐに終了し、.NETレベルのメソッドは順次シーケンシャルに呼び出されるイメージです(でないとドライバに影響してしまうので)。
で、もしそうでない場合は、ドライバ割り込みレベルだとlockですら使うべきでなかったりする場合もあると思いますので、APIの条件というか仕様というか、仕組みというかで何かそういう情報が記載されていないかよく確認するほうがいいと思います。
※という辺りで、.NETレベルのAPIなら普通は割り込みと直接連結はしてないだろうと感じる。
割り込みと直結してない場合は、まずまず普通の操作はできますので、lockなどを使っても大丈夫になっていると思います。
スレッド側とのタイミング調整というか、連携を具体的にどのようにするかって考えてます?
双方向のやり取りみたいなので結構ややこしい気がします(スレッド関連の制御に慣れているなら大丈夫でしょうけど)。
-
他社製品の情報なので、あくまで参考まで、ですが・・・。
様々なI/O機器を統一的なデバイスドライバで動かす、さらに、ラッパーの.Net Frameworkライブラリや、ネイティブな.Net Frameworkライブラリを提供する、そういうベンダーがありますが、基本的になちゃさんが仰るとおりの仕様になっています。
デバイスドライバのレベルでは割り込みやDMAが駆使されていますが、.Net FrameworkのAPIの層では、意識する必要がありません。
逆に言うと、そこまでベンダーがやってくれて.Net FrameworkのAPIの有難味があるわけで、その恩恵に浴せばよいのじゃないかと思います。
ちなみに、小生は、タイマーとして動作するカウンタ情報(32bit長)を読取るプログラムを作ったことがありますが、DMAでメモリにドカっと貯められたデータ(ちゃんとマネージドな形になっている)を、イベントを契機に読み取って処理するもので、数万読み取り/秒は、楽にこなせました。(十年以上前の、.Net Framework 1.1の時代ですが)
- 編集済み 外池 2016年7月20日 7:50
-
Windowsフォームアプリ(WPFに非ず)、.Net Framework 3.5、Windows 7の場合ですが、
純粋にソフトウェア的に、別スレッドから親フォームのInvokeメソッドを繰り返し呼び出してみました。
以下、平均的なパフォーマンスの議論であって、厳密なタイミングは度外視しています。
別スレッドの中で待機時間を設けない、若干待機時間を設ける等により、Invokeする間隔を調整したところ、500Hzでも1000Hzでも問題ありませんでした。待機時間を設けずに立て続けに呼び出したところ、3万数千Hz~4万弱Hzぐらいでした。ただし、Invokeされた側では、1秒に1回だけUIの更新(親フォームのTextプロパティー)を行い、上記のような頻度情報を表示させています。Windows全体のパフォーマンスに影響もありません。UIの更新(親フォームのTextプロパティー)をInvokeされる度に全て行う場合でも、500Hzや1000Hzでは問題ありませんでした。ただし、実際の再描画の頻度はこれほど高くはないと思います。あくまで、Invokeを受け付けてくれている、ということ。待機時間を設けずに立て続けに呼び出す場合には、Windows全体のUIがものすごく遅くなりました。
小生の知る限り、特に、.Net Frameworkでアプリプログラム本体を走らせる場合、ハードウェアのデバイスドライバレベルで割り込みやDMAが行われても、Windowsとアプリプログラムの間ではメッセージの形でやりとりが行われると理解しています。このメッセージの頻度として、数百Hz程度は何の問題も無いよう思われます。
外部機器からのデータの取り込み1回が4byte、
1回ごとに何か処理をしないといけない、
取り込み&処理の頻度が1000Hz
だとして・・・、
生データをHDDに吐き出すとしても4kbyte/secのレートであり、問題になるとは思えません。4wordがもし4×64bit=32byteだとしても、まだまだ大丈夫でしょう・・・。
- 編集済み 外池 2016年7月21日 2:40
-
厳密なタイミングが必要な場合、基本的に、Windowsに頼ってはいけません。特に1秒より小さい時間の精度が必要な場合。
まず、データの読み取り時刻が重要な場合には、Input機器に正確な時計を持たせて、データそのものとデータを得た時刻の両方を読み取ることが必要です。そうすれば、多少Windowsがもたついても、最終的なデータや処理結果は正確なタイミングに基づいており、OKです。
一方で、データを読み取り、処理し、フィードバックするサイクルの時間間隔が厳密でないといけない場合は、Windowsは使えないと思います。読み取りレートと、フィードバックのレートは、I/O機器で精密に整えつつ、フィードバックの時間遅れが秒単位を許してもらえるなら、まぁ、なんとかなるとは思いますが・・・。フィードバック時間遅れが1/500秒とか1/1000秒で一定させないといけない場合、Windowsのちょっとしたもたつきが致命的です。
- 編集済み 外池 2016年7月21日 2:52
-
皆様
いろいろとご検討ありがとうございます。
やりたいことのイメージが伝わりにくいようなので、簡単な例を記載させていただきたいと思います。
たとえば、スレッドの方で、水温、水圧のデータをAD変換等で計測しているとします。
このデータを、パルスカウンタボードのカウント値が一つ増えるごとに、HDDに保存したい、というような用途です。
パルスの最大周波数は500Hzくらいなので、割り込みを使用せず、
1msecごとに、パルス、水温、水圧を取得し、パルス数に変化があったら、
その時のそれらの値をHDDに書けば、そこそこ対応できると思いますが、
スレッドだと、なんらかの要因でPC全体が重いときに1パルスごとの値が取得できないのではないかと考え、
割り込みをかけて計測値の取得を行おうと考えていました。
実際には、数パルス分のデータは取りこぼしても問題ない用途なのですが、入力したパルスのパルス数によって、
他の計測システムの計測データと同期をとるようにいるので、取りこぼしは少ない方が良いです。
他の計測システムとの同期も、本当に厳密にやらなくてはいけない、というわけではないので、
取りこぼしが少ない方がよいという程度です。
-
イメージ、よくわかったような気がします。(独り合点かもしれませんが)
カウンタ自体が、時計になっていますね。そのタイミングに合わせて、他のプロセス測定値を取り込みたいわけですね。
他の計測システムとの同期とか、取りこぼしが多少あっても良いとか・・・、ということは、カウンタの値とプロセス測定値をペアで記録するわけですね? 他の計測システムも同じようにカウンタの値を記録している。 あとでカウンタの値の記録を突き合わせて、同期させて整理する。欠測があっても良いが、カウンタの値を頼りに同期を取れることが大事。・・・・ あってます?
であれば、カウンタの値をWindows PCのプログラムで監視して、その変化でトリガしてプロセス測定値を取り込む方法は止めた方がいいです。数百Hzのレートだとしたら、数周期分ぐらい、すぐにズレそうに思います。外付けのI/Oボードの方で、カウンタ値の変化(増加)タイミングに同期して、プロセス値を測定し、カウンタ値とプロセス測定値をペアにしてWindows PCに取り込むべきです。
- 編集済み 外池 2016年7月21日 12:25
-
外池様
アドバイスありがとうございます。
> 数百Hzのレートだとしたら、数周期分ぐらい、すぐにズレそうに思います。
カウント値はカウンタボードで積算されており、PCではカウント値とプロセス量はペアでHDDに保存しているので、
PC側でカウント変化ごとの保存に取りこぼしがあっても、時間的なずれが蓄積していくことはないと考えます。
同期対象の別の計測システムでも、カウント値とプロセス量をペアで保存しているので、
記録のカウント値を双方で比較して、対応をとることはできます。
(完全に同じカウント値同志で突合せしなくても、一番近いカウント値が近いデータ同志の突合せでも
用途的には問題ありません。)
以上のことから、パルスカウンタボードの割り込みで呼び出される関数で、カウント値とプロセス量を
ペアにしたデータを作成し、それをスレッドに渡して保存しようと考えていましたが、
割り込みによる関数読み出しをやめて、カウント値を監視するスレッドを1本立てて、カウント値が
変化したらカウント値とプロセス量をペアにして保存するようにしようと思います。
(メモリへの書き込みはカウント値が変化するごとに行いますが、それをHDDに書き込むのは10回分ずつまとめて実施、
のようなやり方でもよいと思います)
パルスレートは1~500Hz程度の範囲まで変化しますが、レートの高いときは、データの保存を間引いてもよいかな、
と思い始めました。パルスレートの変化は緩やかなので、過去1秒くらいのパルス変化量を見てパルスレートを推測し、
レートの高いときは4個飛ばしとか、8個飛ばしで保存する、としてもよいと思います。
以上のような方法でやってみようと思います。
ありがとうございました。
-
なるほど。常にカウンタ値(時刻情報)とプロセス測定値をペアにしてWindows PCに取り込んでいるんですね。ならば、500Hzの取り込みレートは、まったく問題ないと思います。取り込みのレートが「測定中であっても」変化するということを初めて理解しましたが、だからこそ、カウンタ値が測定データの一部なわけですよね。
本件、要するに、データをWindows PC上に取込んだ後の、プログラムによる取捨選択の問題と理解しました。
この程度の取り込みレートであれば「全部」保存しても良いんじゃないでしょうか? レートが変化するということは、速いレートにすることに意味があるんだと思います。これを適当に間引いて良いなら、そもそもレート設定が速すぎるということかと。
計測というのは、往々にして不具合を起こします。パワー・サージなども含めて。ノイズを拾ってしまうことも有り得ます。そんな時、他のシステムの測定データと500Hzの1サイクルごとにデータを比較したくなって、間引いた保存を後悔するかも・・・。(小生の経験則にすぎないので、杞憂であればスルーして下さい)
-
パルスカウンタボードと勘違いしていましたが、パルスカウンタとアナログ値が同期しているという事は、カウンタ内蔵のADCですかね?
マニュアルを見ただけで出来るか自身が無いですが
CONTECのボードは、バスマスタ転送はサポートしていないようですが、バッファは搭載しているので、レートに応じて、1〜100位ずつ貯めて読み込んではどうでしょうか?
1Kデータ貯められるので、水温と水圧なら500データ貯められます。
500Hzなら100データまとめてなら200msecで1回、50データなら100msecとか。
1Hzなら勿論、1データずつで1sec.
測定しながら比較レジスタを変更出来るかやってみないと分かりませんが、変更出来れば動いてくれるのではないかと思います。
- 編集済み tmori3y2 2016年7月22日 3:40
-
.NET Frameworkでは開発者でなく.NET Framework自身がメモリ管理を行っています。その点は理解されていると思いますが、このことで一つ注意が必要なのは、.NET Framework側の都合でメモリ移動が行われることがあります。これについて、.NETプログラムであればメモリ移動に追従するため問題になりません。しかし、外部ハードウェアやドライバーなど.NET Frameworkの及ばないプログラムについては指定されたメモリアドレスに直接アクセスしてしまうため、このメモリ移動が発生すると移動前の誤ったアドレスにアクセスすることになり、メモリ破壊を引き起こします。
「このデータは誰が書き込みを行い誰が呼び出しを行うのでしょうか? どちらもプログラムコード、つまり外部ハードウェアが読み書きを行うわけではない、ということでしょうか?」と質問したのはこのためです。アドレスでアクセスするのかそれともライブラリが値を扱うのか、が重要になります。
逆に他の方々はハードウェアについていろいろと議論されているにもかかわらず、この点を言及されないことを不思議に思います。アドレスであれば適切なタイミングで読み書きを行わなければ正しい値とはなりません。しかしライブラリがセンサを読み取り値として渡してくれるのであれば、後はもう.NETの世界でありあまり気にかけることもないかと。
というのも扱うものが4byte x 4 = 16byte程度を500Hzつまり、32byte/secです。1時間で100KB少々です。その程度であれば、割り込み処理の中で、コレクションに追加する等でも構わないのでは? 具体的にはSystem.Collections.Concurrent 名前空間にスレッドセーフなコレクションが用意されています。
- 回答の候補に設定 佐祐理 2016年7月27日 7:39
-
ご回答遅くなり申し訳ありません。
外部ハードウェアにアクセスするのは、ボードメーカ作成のライブラリを使用しますので、
アクセスに関する問題はないと考えます。
> 具体的にはSystem.Collections.Concurrent 名前空間にスレッドセーフなコレクションが用意されています。
これは、大変便利そうですね。
ありがとうございました。
-
パルス周波数のプロファイルが分かっていて測定が不要で有れば、CONTECのアナログ入出力ボードのマニュアルを見る限り、カウンタ搭載、非搭載によらず、外部パルスに同期したAD変換値のバッファリングができるので、割り込みすら不要で、バッファがあふれないように、10~100msec周期のスレッドでバッファリングされたデータをコレクションクラスにコピーするだけで充分だと思います。
この場合は、カウンタ値は1カウントずつアップするだけなので不要の筈です。欲しければコレクションのインデックスに1を足せば得られます。
1カウント毎に割り込みをかけてタイムスタンプを取得する場合は、外池さんのおっしゃる通り、参考程度の誤差のあるものです。あくまでも測定結果は外部パルスに同期しているバッファリングしたデータです。
コピーはスレッドではなく割り込みでします。2点以上貯まっていた時はタイムスタンプの補正が必要です。
この場合のカウンタは内蔵もしくは別ボードのものが使用できます。
どちらの方法にせよ、割り込みとスレッド両方使う必要はないです
- 編集済み tmori3y2 2016年7月27日 23:47