![]() |
libusb 1.0.24
USBデバイスにアクセスするためのクロス・プラットフォームのユーザー・ライブラリ
|
libusbはスレッド・セーフなライブラリですが、複数のスレッドからlibusbと対話するアプリケーションには追加の考慮事項を適用する必要があります。
対処しなければならない根本的な問題は、すべてのlibusb入出力が poll()/select() システム・コールによるファイル・デスクリプターの監視を中心に展開していることです。これは 非同期インターフェイス では剥き出しになっていますが、 同期インターフェイス は非同期インターフェイスの上に実装されるため、同じ考慮事項が適用されることに重々注意してください。
問題は、2つ以上のスレッドがlibusbのファイル記述子で poll() または select() を同時に呼び出している場合、イベントが到着したときにそれらのスレッドの1つだけがウェイク・アップされることです。 他のスレッド達は何かが起こったことに全く気づかないでしょう。
以下の擬似コードについて考えてみます。この擬似コードは、非同期転送を送信してから、その完了を待ちます。このスタイルは、非同期インターフェイスの上に同期インターフェイスを実装できる方法の一つです(このページで説明されている複雑さのために、より高度ですが、libusbは同様のことを行います)。
ここでは、条件に対して非同期イベントの完了を直列化しています。条件は特定の転送の完了です。 poll() ループには、何も起こらない状況でのCPU使用率を最小限に抑えるために、長いタイムアウトがあります(わりと無制限である可能性があります)。
これがlibusbのファイル・デスクリプターをポーリングしている唯一のスレッドである場合、問題はありません。別のスレッドが関心のあるイベントを飲み込む危険はありません。一方、同じデスクリプターをポーリングしている別のスレッドがある場合
、関心のあるイベントを受信する可能性があります。この状況では、 myfunc()
は、最大120秒後のループの次の反復で転送が完了したことのみを認識します。明らかに2分の遅延は望ましくありませんが、だからと言って、この問題を回避するために短いタイムアウトを使用することは考えないでください。
ここでの解決策は、2つのスレッドがファイル・デスクリプターを同時にポーリングしないようにすることです。 これの単純な実装はライブラリの機能に影響を与えるため、libusbは、機能が失われないようにするために、以下に説明する仕組みを提供します。
先に進む前に、libusbで覆われたすべてのイベント処理手順が以下に記載されている仕組みに完全に準拠していることに言及する価値があります。これには、 libusb_handle_events() とその変種、およびすべての同期入出力O関数が含まれます。libusbは、この頭痛の種をあなたから隠します。
libusb_handle_events() と同期入出力関数のみを使用している場合でも、競合状態が発生する可能性があります。以下のように libusb_handle_events() を使用して、上記を解決したくなるかもしれません:
ただし、これには、完了のチェックと libusb_handle_events() によるイベント・ロック取得との間で競合があるため、別のスレッドが転送を完了し、タイムアウトまたは別のイベントが発生するまでこのスレッドがハングする可能性があります。libusbの同期API実装でこれを修正する commit 6696512aade99bb15d6792af90ae329af270eba6 も参照してください。
この競合を修正するには、イベント・ロックを取得した後にのみ完了した変数をチェックする必要があります。つまり、ロックを気にせずに libusb_handle_events() を呼び出すという考え方は通用しなくなります。 これが、libusb-1.0.9が新しい libusb_handle_events_timeout_completed() 関数と libusb_handle_events_completed() 関数を導入した理由です。これらの関数は、ロックを取得した後に完了チェックを実行します。
これにより、我々の例の競合がうまく修正されます。なお、単一の転送を送信してその完了を待つだけの場合は、同期入出力機能の1つを使用する方がはるかに簡単であることに注意してください。
complete
変数は、イベント・ロックを保持しつつ変更する必要があります。そうしないと、競合状態が引き続き存在する可能性があります。上記のように転送コールバック内からこれを行うのが最も簡単です。問題は、libusbがファイル・デスクリプターを公開して、非同期USB入出力を既存のメイン・ループに統合できるようにし、libusbの背後で効果的に作業を行えるようにするという事実を考慮する場合です。libusbのファイル・デスクリプターを取得して、自分で poll()/select() に渡す場合は、関連する問題に注意する必要があります。
導入される最初の概念は、イベント・ロックです。 イベント・ロックは、イベントを処理するスレッド達を直列化するために使用されます。これにより、一度に1つのスレッドのみがイベントを処理します。
あなたは、 libusb_lock_events() を使用してlibusbファイル・デスクリプターをポーリングする前に、イベント・ロックを取得する必要があります。 libusb_unlock_events()を使用して poll()/select() ループを中止したら、すぐにロックを解除する必要があります。
イベント・ロックは解決策の重要な部分ですが、それだけでは十分ではありません。 あなたは以下で十分かどうか疑問に思うかもしれません…
…そして答えはこれではありません。なぜなら、これは、上記のコードの転送が完了するまでに長い時間(たとえば30秒)かかる場合があり、転送が完了するまでロックが解除されないためです。
イベント処理を実行したい同様のコードを持つ別のスレッドが、数ミリ秒後に完了する転送で動作している可能性があります。完了までの時間が非常に短いにもかかわらず、他のスレッドは、ロックの競合のために上記のコードが終了するまで(30秒後まで)転送のステータスを確認できません。
これを解決するために、libusbは、別のスレッドがいつイベントを処理しているかを判別するメカニズムを提供します。また、イベント処理スレッドがイベントを完了するまでスレッドをブロックするメカニズムも提供します(このメカニズムには、ファイル・デスクリプターのポーリングは含まれません)。
別のスレッドが現在イベントを処理していることを確認した後、 あなたは libusb_lock_event_waiters() を使用してイベント待ちロックを取得します。 次に、あなたは、他のスレッドがまだイベントを処理していることを再確認し、処理している場合は、 libusb_wait_for_event() を呼び出します。
libusb_wait_for_event() は、イベントが発生するまで、またはスレッドがイベント・ロックを解放するまで、あなたのアプリケーションをスリープ状態にします。これらのどれかが発生すると、スレッドがウェイクアップされるので、待機していた状態を再確認する必要があり、また、別のスレッドがイベントを処理していることを再確認する必要があります。そうでない場合は、イベント自体の処理を開始する必要があります。
これは、擬似コードとして以下のようになります:
上記のコードをざっと見ると、これは1つのイベント待ちしかサポートできない(したがって、合計2つの競合するスレッドで、もう1つはイベント処理を実行しています)、イベント待ちがイベントを待っている間にイベント待ちをロックしたように見えます。ただし、 libusb_wait_for_event() は待機中に実際にロックを解除し、続行する前にロックを再取得するため、システムは複数のイベント待ちをサポートします。
我々は、これで、誰もイベントを処理していない状況を動的に処理できるコードを実装しました(つまり、自分で処理する必要があります)。また、別のスレッドがイベント処理を実行している状況も処理できます(つまり、それらに便乗できます)。また、2つの組み合わせを処理する機能も備えています。たとえば、別のスレッドがイベント処理を実行していますが、何らかの理由で条件が満たされる前に処理を停止するため、イベント処理を引き継ぎます。
上記の擬似コードでは、4つの関数が導入されました。 それらの重要性は、上記のコードから明らかです。
あなたは libusb_wait_for_event() でブロックされたすべてのスレッドをウェイクアップする関数が無いのを疑問に思われるかもしれません。これは、libusbがこれを内部で実行できるためです。誰かが libusb_unlock_events() を呼び出したとき、または転送が完了したとき(コールバックが戻った後の時点)で、そのようなすべてのスレッドをウェイクアップします。
上記の説明で普通は十分ですが、あなたが問題についてさらに深く考察している場合、libusbの内部に関するいくつかの質問が残っているかもしれません。もし興味がある場合は以下を読み進めてください。そうでない場合は、混乱を避けるためにこの節は飛ばして構いません。
まず頭に浮かぶ質問は、別のスレッドがイベント処理を行っている間に、あるスレッドがポーリングする必要のあるファイル・デスクリプターのセットを変更した場合はどうなるか?ということです。
これが発生する可能性がある状況は2つあります。
libusbはこれらの問題を内部で処理するため、アプリケーション開発者はデバイスをオープンしたりクローズしたりするときにイベント・ハンドラーを停止する必要はありません。最初に libusb_close() の状況に焦点を当てて、その仕組みを説明します:
libusb_open() も同様ですが、実際にはもっと単純なパターンです。 libusb_open() の呼び出し時は以下のとおりです:
上記は少し複雑に思えるかもしれませんが、望み通りであるためには、なぜそのような複雑さが必要なのかを明らかにしました。 また、これはlibusbのファイル・デスクリプターを取得して独自のポーリング・ループに統合するアプリケーションにのみ適用されることを忘れないでください。
あなたの2つのスレッドが同時にデスクリプターをポーリングできるとは思わない時、マルチ・スレッド・アプリケーションが上記のルールとロックの一部を無視しても問題ないと判断する場合があります。その場合は、その心配する必要がないので、それはあなたにとって朗報です。ただし、同期入出力関数は内部でイベント処理を行うことに注意してください。1つのスレッドがループ内でイベント処理を実行し(上記の規則とロック方法論を実装せずに)、別のスレッドが同期USB転送を送信しようとすると、2つのスレッドが同じデスクリプターを監視することになり、上述の望ましくない振る舞いが発生します。その解決策は、あなたがポーリング・スレッドを規則に従って上演することです。同期入出力関数はそのように動き、そしてその結果はこれらが完全に調和する結果になります。
イベント処理を行う専用スレッドがある場合、イベント処理ロックを長期間取得することは完全に合法です。他のスレッドから呼び出す同期入出力関数は、上記の「イベント待ち」メカニズムに透過的にフォールバックします。イベント処理スレッドが適用しなければならない唯一の考慮事項は、 libusb_event_handling_ok() に関連するものです。すべての poll() の前にこれを呼び出し、指示された場合はイベント・ロックを放棄する必要があります。