libusb  1.0.24
USBデバイスにアクセスするためのクロス・プラットフォームのユーザー・ライブラリ
マルチ・スレッド・アプリケーションと非同期入出力

libusbはスレッド・セーフなライブラリですが、複数のスレッドからlibusbと対話するアプリケーションには追加の考慮事項を適用する必要があります。

対処しなければならない根本的な問題は、すべてのlibusb入出力が poll()/select() システム・コールによるファイル・デスクリプターの監視を中心に展開していることです。これは 非同期インターフェイス では剥き出しになっていますが、 同期インターフェイス は非同期インターフェイスの上に実装されるため、同じ考慮事項が適用されることに重々注意してください。

問題は、2つ以上のスレッドがlibusbのファイル記述子で poll() または select() を同時に呼び出している場合、イベントが到着したときにそれらのスレッドの1つだけがウェイク・アップされることです。 他のスレッド達は何かが起こったことに全く気づかないでしょう。

以下の擬似コードについて考えてみます。この擬似コードは、非同期転送を送信してから、その完了を待ちます。このスタイルは、非同期インターフェイスの上に同期インターフェイスを実装できる方法の一つです(このページで説明されている複雑さのために、より高度ですが、libusbは同様のことを行います)。

void cb(struct libusb_transfer *transfer)
{
int *completed = transfer->user_data;
*completed = 1;
}
void myfunc() {
struct libusb_transfer *transfer;
unsigned char buffer[LIBUSB_CONTROL_SETUP_SIZE] __attribute__ ((aligned (2)));
int completed = 0;
transfer = libusb_alloc_transfer(0);
libusb_fill_control_transfer(transfer, dev, buffer, cb, &completed, 1000);
while (!completed) {
poll(libusb file descriptors, 120*1000);
if (poll indicates activity)
}
printf("completed!");
// other code here
}

ここでは、条件に対して非同期イベントの完了を直列化しています。条件は特定の転送の完了です。 poll() ループには、何も起こらない状況でのCPU使用率を最小限に抑えるために、長いタイムアウトがあります(わりと無制限である可能性があります)。

これがlibusbのファイル・デスクリプターをポーリングしている唯一のスレッドである場合、問題はありません。別のスレッドが関心のあるイベントを飲み込む危険はありません。一方、同じデスクリプターをポーリングしている別のスレッドがある場合 、関心のあるイベントを受信する可能性があります。この状況では、 myfunc() は、最大120秒後のループの次の反復で転送が完了したことのみを認識します。明らかに2分の遅延は望ましくありませんが、だからと言って、この問題を回避するために短いタイムアウトを使用することは考えないでください。

ここでの解決策は、2つのスレッドがファイル・デスクリプターを同時にポーリングしないようにすることです。 これの単純な実装はライブラリの機能に影響を与えるため、libusbは、機能が失われないようにするために、以下に説明する仕組みを提供します。

先に進む前に、libusbで覆われたすべてのイベント処理手順が以下に記載されている仕組みに完全に準拠していることに言及する価値があります。これには、 libusb_handle_events() とその変種、およびすべての同期入出力O関数が含まれます。libusbは、この頭痛の種をあなたから隠します。

複数スレッドからの libusb_handle_events() 呼び出し

libusb_handle_events() と同期入出力関数のみを使用している場合でも、競合状態が発生する可能性があります。以下のように libusb_handle_events() を使用して、上記を解決したくなるかもしれません:

while (!completed) {
}
printf("completed!");

ただし、これには、完了のチェックと libusb_handle_events() によるイベント・ロック取得との間で競合があるため、別のスレッドが転送を完了し、タイムアウトまたは別のイベントが発生するまでこのスレッドがハングする可能性があります。libusbの同期API実装でこれを修正する commit 6696512aade99bb15d6792af90ae329af270eba6 も参照してください。

この競合を修正するには、イベント・ロックを取得した後にのみ完了した変数をチェックする必要があります。つまり、ロックを気にせずに libusb_handle_events() を呼び出すという考え方は通用しなくなります。 これが、libusb-1.0.9が新しい libusb_handle_events_timeout_completed() 関数と libusb_handle_events_completed() 関数を導入した理由です。これらの関数は、ロックを取得した後に完了チェックを実行します。

while (!completed) {
}
printf("completed!");

これにより、我々の例の競合がうまく修正されます。なお、単一の転送を送信してその完了を待つだけの場合は、同期入出力機能の1つを使用する方がはるかに簡単であることに注意してください。

注意
complete変数は、イベント・ロックを保持しつつ変更する必要があります。そうしないと、競合状態が引き続き存在する可能性があります。上記のように転送コールバック内からこれを行うのが最も簡単です。

イベント・ロック(lock)

問題は、libusbがファイル・デスクリプターを公開して、非同期USB入出力を既存のメイン・ループに統合できるようにし、libusbの背後で効果的に作業を行えるようにするという事実を考慮する場合です。libusbのファイル・デスクリプターを取得して、自分で poll()/select() に渡す場合は、関連する問題に注意する必要があります。

導入される最初の概念は、イベント・ロックです。 イベント・ロックは、イベントを処理するスレッド達を直列化するために使用されます。これにより、一度に1つのスレッドのみがイベントを処理します。

あなたは、 libusb_lock_events() を使用してlibusbファイル・デスクリプターをポーリングする前に、イベント・ロックを取得する必要があります。 libusb_unlock_events()を使用して poll()/select() ループを中止したら、すぐにロックを解除する必要があります。

他のスレッドに作業を任せる

イベント・ロックは解決策の重要な部分ですが、それだけでは十分ではありません。 あなたは以下で十分かどうか疑問に思うかもしれません…

while (!completed) {
poll(libusb file descriptors, 120*1000);
if (poll indicates activity)
}

…そして答えはこれではありません。なぜなら、これは、上記のコードの転送が完了するまでに長い時間(たとえば30秒)かかる場合があり、転送が完了するまでロックが解除されないためです。

イベント処理を実行したい同様のコードを持つ別のスレッドが、数ミリ秒後に完了する転送で動作している可能性があります。完了までの時間が非常に短いにもかかわらず、他のスレッドは、ロックの競合のために上記のコードが終了するまで(30秒後まで)転送のステータスを確認できません。

これを解決するために、libusbは、別のスレッドがいつイベントを処理しているかを判別するメカニズムを提供します。また、イベント処理スレッドがイベントを完了するまでスレッドをブロックするメカニズムも提供します(このメカニズムには、ファイル・デスクリプターのポーリングは含まれません)。

別のスレッドが現在イベントを処理していることを確認した後、 あなたは libusb_lock_event_waiters() を使用してイベント待ちロックを取得します。 次に、あなたは、他のスレッドがまだイベントを処理していることを再確認し、処理している場合は、 libusb_wait_for_event() を呼び出します。

libusb_wait_for_event() は、イベントが発生するまで、またはスレッドがイベント・ロックを解放するまで、あなたのアプリケーションをスリープ状態にします。これらのどれかが発生すると、スレッドがウェイクアップされるので、待機していた状態を再確認する必要があり、また、別のスレッドがイベントを処理していることを再確認する必要があります。そうでない場合は、イベント自体の処理を開始する必要があります。

これは、擬似コードとして以下のようになります:

retry:
if (libusb_try_lock_events(ctx) == 0) {
// 我々はイベント・ロックを取得しました: 我々独自のイベント処理を行います
while (!completed) {
goto retry;
}
poll(libusb file descriptors, 120*1000);
if (poll indicates activity)
}
} else {
// 別のスレッドがイベント処理を行っています。
// イベントが完了したことを我々に通知(signal)するのを待ちます
while (!completed) {
// 今やイベント待ちがロックされたので、
// ダブル・スレッドはまだイベントを処理しています。
// (この箇所に到達するまでにイベントの処理を停止した可能性があります)
// イベントを処理している者は誰も居ません。もう一度やり直してください
goto retry;
}
}
}
printf("completed!\n");

上記のコードをざっと見ると、これは1つのイベント待ちしかサポートできない(したがって、合計2つの競合するスレッドで、もう1つはイベント処理を実行しています)、イベント待ちがイベントを待っている間にイベント待ちをロックしたように見えます。ただし、 libusb_wait_for_event() は待機中に実際にロックを解除し、続行する前にロックを再取得するため、システムは複数のイベント待ちをサポートします。

我々は、これで、誰もイベントを処理していない状況を動的に処理できるコードを実装しました(つまり、自分で処理する必要があります)。また、別のスレッドがイベント処理を実行している状況も処理できます(つまり、それらに便乗できます)。また、2つの組み合わせを処理する機能も備えています。たとえば、別のスレッドがイベント処理を実行していますが、何らかの理由で条件が満たされる前に処理を停止するため、イベント処理を引き継ぎます。

上記の擬似コードでは、4つの関数が導入されました。 それらの重要性は、上記のコードから明らかです。

  1. libusb_try_lock_events() は、イベント・ロックを取得しようと試みますが、競合した場合は失敗コードを返す非ブロッキング関数です。
  2. libusb_event_handling_ok() は、あなたのスレッドがイベント処理を実行することをlibusbがまだ不満を持っていないことを確認します。libusbがイベント・ハンドラーに割り込む必要がある場合があり、これにより、割り込まれたかどうかを確認できます。この関数が0を返す場合、正しい動作は、イベント処理ロックを放棄してから、サイクルを繰り返すことです。 次の libusb_try_lock_events() は失敗するため、イベント待ちになります。 詳細については、以下の 全体の流れ をお読みください。
  3. libusb_handle_events_locked() は、イベント・ロックを保持しながら呼び出すことができる libusb_handle_events_timeout() の変種です。 libusb_handle_events_timeout() 自体は上記と同様のロジックを実装しているため、ここでの場合のように、あなたが「libusbの背後で作業している」ときは呼び出さないでください。
  4. libusb_event_handler_active() は、誰かが現在イベント・ロックを保持しているかどうかを判断します

あなたは libusb_wait_for_event() でブロックされたすべてのスレッドをウェイクアップする関数が無いのを疑問に思われるかもしれません。これは、libusbがこれを内部で実行できるためです。誰かが libusb_unlock_events() を呼び出したとき、または転送が完了したとき(コールバックが戻った後の時点)で、そのようなすべてのスレッドをウェイクアップします。

全体の流れ

上記の説明で普通は十分ですが、あなたが問題についてさらに深く考察している場合、libusbの内部に関するいくつかの質問が残っているかもしれません。もし興味がある場合は以下を読み進めてください。そうでない場合は、混乱を避けるためにこの節は飛ばして構いません。

まず頭に浮かぶ質問は、別のスレッドがイベント処理を行っている間に、あるスレッドがポーリングする必要のあるファイル・デスクリプターのセットを変更した場合はどうなるか?ということです。

これが発生する可能性がある状況は2つあります。

  1. libusb_open() は別のファイル・デスクリプターをポーリング・セットに追加するため、イベント・ハンドラーを中断して再起動し、新しいデスクリプターを取得することが望ましいです。
  2. libusb_close() は、ポーリング・セットからファイル・デスクリプターを削除します。ここではさまざまな競合状態が発生する可能性があるため、その時点では誰もイベント処理を行っていないことが重要です。

libusbはこれらの問題を内部で処理するため、アプリケーション開発者はデバイスをオープンしたりクローズしたりするときにイベント・ハンドラーを停止する必要はありません。最初に libusb_close() の状況に焦点を当てて、その仕組みを説明します:

  1. 利用準備(initialization)中に、libusbは内部パイプを開き、この内部パイプの読み取り端をポーリングされるファイル・デスクリプターのセットに追加します。
  2. libusb_close() 処理中に、libusbはこのイベント・パイプにダミー・データを書き込みます。これにより、イベント・ハンドラが直ちに中断されます。libusbは、この優先度の高いイベントのイベント・ハンドラーに割り込もうとしていることも内部的に記録します。
  3. この時点で、上記の関数の一部は異なる動作を開始します:
    • libusb_event_handling_ok() は1を返し始め、イベント処理を続行しても問題がないことを示します。
    • libusb_try_lock_events() は1を返し始め、ロックが競合していない場合でも、別のスレッドがイベント処理ロックを保持していることを示します。
    • libusb_event_handler_active() は1を返し始め、それが真でない場合でも、別のスレッドがイベント処理を行っていることを示します。
  4. 上記の振る舞いの変更により、イベント・ハンドラーはイベント・ロックを非常に迅速に停止および放棄し、優先度の高い libusb_close() 操作にイベント・ロックを取得するための「タダ乗り」を提供します。イベント処理を行うために競合しているすべてのスレッドは、イベント待ちになります。
  5. libusb_close() 内にイベント・ロック達が保持されているので、libusbは、誰もそれらのデスクリプターをポーリングしたり、ポーリング・セットにアクセスしようとしたりしていないことが分かり、ポーリング・セットからファイル記述子を安全に削除できます。
  6. イベント・ロックを取得した後、クローズ操作は非常に迅速に(通常はミリ秒単位で)完了し、すぐにイベント・ロックを解放します。
  7. 同時に、 libusb_event_handling_ok() とその友人達の振る舞いは、文書化された元の行動に戻ります。
  8. イベント・ロックを解除すると、イベントを待機しているスレッドがウェイクアップされ、再びイベント・ハンドラーになるための競合が開始されます。それらのどれか1つは成功します。その後、ポーリング・デスクリプターのリストを再取得し、USB入出力は通常どおり続行されます。

libusb_open() も同様ですが、実際にはもっと単純なパターンです。 libusb_open() の呼び出し時は以下のとおりです:

  1. デバイスがオープンされ、ファイル・デスクリプターがポーリング・セットに追加されます。
  2. libusbはイベント・パイプでダミー・データを送信し、ポーリング・デスクリプター・セットを変更しようとしていることを記録します。
  3. イベント・ハンドラーが割り込み中断され、 libusb_close() と同じ振る舞いの変更が有効になり、すべてのイベント処理スレッドがイベント待ちになります。
  4. libusb_open() のコードは、イベント・ロックにただ乗りでアクセスできます。
  5. イベント・ハンドラーが正常に一時停止されたので、安心して libusb_open() はイベント・ロックを解放します。
  6. イベント待ちスレッドはすべてウェイクアップされ、再びイベント・ハンドラーになるために競合します。そのうちイベント・ハンドラーになる事に成功したものは、新しいデバイスの追加を含むポーリング・デスクリプターのリストを再度取得します。

おわりに

上記は少し複雑に思えるかもしれませんが、望み通りであるためには、なぜそのような複雑さが必要なのかを明らかにしました。 また、これはlibusbのファイル・デスクリプターを取得して独自のポーリング・ループに統合するアプリケーションにのみ適用されることを忘れないでください。

あなたの2つのスレッドが同時にデスクリプターをポーリングできるとは思わない時、マルチ・スレッド・アプリケーションが上記のルールとロックの一部を無視しても問題ないと判断する場合があります。その場合は、その心配する必要がないので、それはあなたにとって朗報です。ただし、同期入出力関数は内部でイベント処理を行うことに注意してください。1つのスレッドがループ内でイベント処理を実行し(上記の規則とロック方法論を実装せずに)、別のスレッドが同期USB転送を送信しようとすると、2つのスレッドが同じデスクリプターを監視することになり、上述の望ましくない振る舞いが発生します。その解決策は、あなたがポーリング・スレッドを規則に従って上演することです。同期入出力関数はそのように動き、そしてその結果はこれらが完全に調和する結果になります。

イベント処理を行う専用スレッドがある場合、イベント処理ロックを長期間取得することは完全に合法です。他のスレッドから呼び出す同期入出力関数は、上記の「イベント待ち」メカニズムに透過的にフォールバックします。イベント処理スレッドが適用しなければならない唯一の考慮事項は、 libusb_event_handling_ok() に関連するものです。すべての poll() の前にこれを呼び出し、指示された場合はイベント・ロックを放棄する必要があります。

LIBUSB_ENDPOINT_OUT
@ LIBUSB_ENDPOINT_OUT
Out: ホストからデバイスへ。
定義: libusb.h:319
libusb_event_handler_active
int libusb_event_handler_active(libusb_context *ctx)
定義: io.c:1887
libusb_handle_events
int libusb_handle_events(libusb_context *ctx)
定義: io.c:2407
libusb_transfer::buffer
unsigned char * buffer
Data buffer.
定義: libusb.h:1273
libusb_fill_control_setup
static void libusb_fill_control_setup(unsigned char *buffer, uint8_t bmRequestType, uint8_t bRequest, uint16_t wValue, uint16_t wIndex, uint16_t wLength)
定義: libusb.h:1525
libusb_unlock_events
void libusb_unlock_events(libusb_context *ctx)
定義: io.c:1823
libusb_alloc_transfer
struct libusb_transfer * libusb_alloc_transfer(int iso_packets)
定義: io.c:1285
libusb_handle_events_completed
int libusb_handle_events_completed(libusb_context *ctx, int *completed)
定義: io.c:2429
libusb_unlock_event_waiters
void libusb_unlock_event_waiters(libusb_context *ctx)
定義: io.c:1963
libusb_submit_transfer
int libusb_submit_transfer(struct libusb_transfer *transfer)
定義: io.c:1489
libusb_lock_event_waiters
void libusb_lock_event_waiters(libusb_context *ctx)
定義: io.c:1952
libusb_lock_events
void libusb_lock_events(libusb_context *ctx)
定義: io.c:1808
libusb_event_handling_ok
int libusb_event_handling_ok(libusb_context *ctx)
定義: io.c:1858
libusb_handle_events_timeout
int libusb_handle_events_timeout(libusb_context *ctx, struct timeval *tv)
定義: io.c:2387
libusb_wait_for_event
int libusb_wait_for_event(libusb_context *ctx, struct timeval *tv)
定義: io.c:1995
libusb_transfer
定義: libusb.h:1222
libusb_transfer::user_data
void * user_data
User context data.
定義: libusb.h:1270
libusb_handle_events_locked
int libusb_handle_events_locked(libusb_context *ctx, struct timeval *tv)
定義: io.c:2457
LIBUSB_REQUEST_TYPE_VENDOR
@ LIBUSB_REQUEST_TYPE_VENDOR
Vendor.
定義: libusb.h:405
libusb_try_lock_events
int libusb_try_lock_events(libusb_context *ctx)
定義: io.c:1765
libusb_fill_control_transfer
static void libusb_fill_control_transfer(struct libusb_transfer *transfer, libusb_device_handle *dev_handle, unsigned char *buffer, libusb_transfer_cb_fn callback, void *user_data, unsigned int timeout)
定義: libusb.h:1574