macOSからPi Picoを使う - その30 [Pi Pico]
libusbの非同期転送の続き。非同期転送はlibsusb-1.0の目玉機能らしい。非同期転送そのものは、慣れた人にはそれほど難しくなく、わかりやすくまとめられているので苦労は少ないはず。しかしmulti-threadを前提とした他の非同期転送を使い慣れていると、それと同じようにlibusbも使えばいいと思ってしまうが....
ところがそうではない。
libusbの内部ではthreadを起こしていない、とドキュメントにある。それはlibusbの設計思想によるらしいけど、ユーザとしては他に影響が出なければ勝手にやってもらっていいのに、という気はしないでもない。
そうすると非同期転送はどうするか、というと
という関数を定期的に呼ぶ、ということをしないといけない。この関数の中で非同期転送の処理が行われて、必要なときにここからコールバックが呼ばれる。古い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の動作をよく知っている必要があって結構めんどくさい。
というのが紹介されている。これで例えばpthreadを使って
とでもすればいい。Windowsではこうする必要がある、とDocumentには書かれているけど、僕はWindowsのことは全く知らないので、その理由は理解できない。
この解決の1番の問題はコールバック関数がこのthreadから呼ばれる、という点である。そのままだとmain threadで非同期転送をsubmitしたのに、転送終了処理はこっちのthreadになる。もう絵に描いたようなthread unsafeな事態を招くプログラミングで、排他制御やアトミック動作を組み込んで真面目にやらないと痛い目に会う。
macOSの場合、べつのthreadで関数を呼ぶ、ということができるので、コールバックの中で、必要な終了処理をmain threadで起動するように書けばいい。例えばObjective-Cで
あるいはCだとGCDを呼んで
SwiftでもGCDで
などとすればいい。
Objective-Cでの動作は実際にはNSRunLoopの機能が使われていて、ちょっと二度手間感がある。GCDの場合はドキュメントによると
17.6 非同期転送のコールバックはどこから呼ばれるのか?
非同期転送のコールバックはどこから呼ばれるのか、って、そりゃあ、libusb内部で割り込みか、あるいはループが回っていて、そこから起動されるんだろう、転送をサブミットしたあと他のことをやってる間libusbが止まってたらダメじゃん、でないと非同期転送の意味がないじゃん、なんて僕は思っていた。ところがそうではない。
libusbの内部ではthreadを起こしていない、とドキュメントにある。それはlibusbの設計思想によるらしいけど、ユーザとしては他に影響が出なければ勝手にやってもらっていいのに、という気はしないでもない。
そうすると非同期転送はどうするか、というと
int libusb_handle_events(libusb_context *ctx)
ただし、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; }
#include <pthread.h> // pthread_t thread; if (pthread_create(&thread, NULL, event_thread_func, ctx) != 0) { handle_error(); } //
この解決の1番の問題はコールバック関数がこのthreadから呼ばれる、という点である。そのままだとmain threadで非同期転送をsubmitしたのに、転送終了処理はこっちのthreadになる。もう絵に描いたようなthread unsafeな事態を招くプログラミングで、排他制御やアトミック動作を組み込んで真面目にやらないと痛い目に会う。
macOSの場合、べつのthreadで関数を呼ぶ、ということができるので、コールバックの中で、必要な終了処理をmain threadで起動するように書けばいい。例えばObjective-Cで
[obj performSelectorOnMainThread:@slector(transferCompletion)];
dispatch_queue_main_t mainQueue = dispatch_get_main_queue(); dispatch_async_f(mainQueue, NULL, transferCompletion);
DispatchQueue.main.async { self.transferCompletion() }
Objective-Cでの動作は実際にはNSRunLoopの機能が使われていて、ちょっと二度手間感がある。GCDの場合はドキュメントによると
- dispatch_main()を呼ぶ
- NSApplicationMain(_:_:)を呼ぶ
- CFRunLoopRefを使う
2021-09-12 21:25
nice!(0)
コメント(0)
コメント 0