SSブログ

macOSからPi Picoを使う - その30 [Pi Pico]

libusbの非同期転送の続き。非同期転送はlibsusb-1.0の目玉機能らしい。非同期転送そのものは、慣れた人にはそれほど難しくなく、わかりやすくまとめられているので苦労は少ないはず。しかしmulti-threadを前提とした他の非同期転送を使い慣れていると、それと同じようにlibusbも使えばいいと思ってしまうが....

17.6  非同期転送のコールバックはどこから呼ばれるのか?

非同期転送のコールバックはどこから呼ばれるのか、って、そりゃあ、libusb内部で割り込みか、あるいはループが回っていて、そこから起動されるんだろう、転送をサブミットしたあと他のことをやってる間libusbが止まってたらダメじゃん、でないと非同期転送の意味がないじゃん、なんて僕は思っていた。

ところがそうではない。

libusbの内部ではthreadを起こしていない、とドキュメントにある。それはlibusbの設計思想によるらしいけど、ユーザとしては他に影響が出なければ勝手にやってもらっていいのに、という気はしないでもない。

そうすると非同期転送はどうするか、というと
int 	libusb_handle_events(libusb_context *ctx)
という関数を定期的に呼ぶ、ということをしないといけない。この関数の中で非同期転送の処理が行われて、必要なときにここからコールバックが呼ばれる。古いMac OSでWaitNextEvent()関数を定期的に呼ばないといけなかったのとまったく同じである(WaitNextEvent()なんてもうほとんど誰も知らないだろうなあ)。

ただし、MacOSでのWaitNextEvent()と違って、このlibusb_handle_events()関数はなんらかのUSBイベントが発生するまでブロックする。この動作は実装する上では結構悩ましい。その話はまたさらにあとでする。

今のmacOSではFoundation frameworkを使うとthreadにひとつRunLoopが回る。正確に言うなら、単にthreadを起こしただけではRunLoop起きないが、RunLoopを必要とする操作が行われるとそれに先立って自動的にRunLoopがひとつだけ起動されることになっている。

従ってmacOSではこのRunLoopの中にlibusb_handle_events()関数を組み込むのがスジである。

しかしそれはさっき書いたlibusb_handle_events()のブロックする仕様が問題になる。libusbの他の機能(イベントポーリング)を使わずにlibusb_handle_events()関数を直接使うにはRunLoopの動作をよく知っている必要があって結構めんどくさい。

17.6.1  独立したthreadでlibusb_handle_events()

一つの解決はこのlibusb_handle_events()を定期的に呼ぶためのthreadを起こすことである。libusbのDocumentには一番簡単な例として
void *event_thread_func(void *ctx)
{
    while (event_thread_run)
        libusb_handle_events(ctx);
    return NULL;
}
というのが紹介されている。これで例えばpthreadを使って
#include <pthread.h>
//
    pthread_t thread;

    if (pthread_create(&thread, NULL, event_thread_func, ctx) != 0) {
            handle_error();
    }
//
とでもすればいい。Windowsではこうする必要がある、とDocumentには書かれているけど、僕はWindowsのことは全く知らないので、その理由は理解できない。

この解決の1番の問題はコールバック関数がこのthreadから呼ばれる、という点である。そのままだとmain threadで非同期転送をsubmitしたのに、転送終了処理はこっちのthreadになる。もう絵に描いたようなthread unsafeな事態を招くプログラミングで、排他制御やアトミック動作を組み込んで真面目にやらないと痛い目に会う。

macOSの場合、べつのthreadで関数を呼ぶ、ということができるので、コールバックの中で、必要な終了処理をmain threadで起動するように書けばいい。例えばObjective-Cで
   [obj performSelectorOnMainThread:@slector(transferCompletion)];
あるいはCだとGCDを呼んで
    dispatch_queue_main_t mainQueue = dispatch_get_main_queue();
    dispatch_async_f(mainQueue, NULL, transferCompletion);
SwiftでもGCDで
    DispatchQueue.main.async {
        self.transferCompletion()
    }
などとすればいい。

Objective-Cでの動作は実際にはNSRunLoopの機能が使われていて、ちょっと二度手間感がある。GCDの場合はドキュメントによると
  1. dispatch_main()を呼ぶ
  2. NSApplicationMain(_:_:)を呼ぶ
  3. CFRunLoopRefを使う
のどれかを使う、となっている。みっつめは実質的にObjective-Cでの動作と全く同じだけど、他の場合どう違うのかいまいちよくわかってない。
nice!(0)  コメント(0) 

nice! 0

コメント 0

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。