libus是一个线程安全的库,但是多个线程的libusb相互配合工作需要额外的考虑。
最根本需要解决的问题是所有的libusb I/O 处理都是通过poll()/select() 系统调用监控文件描述符。在asynchronous接口中直接显露出来的,但是同样需要注意synchronous接口是在asynchonrous接口之上实现的,因此同样需要考虑。 这个问题是如果2个以上的线程同时在libusb的文件描述符上调用poll() or select(),这些线程中只有一个会在事件到来时被唤醒,其他的会完全被忽略任何发生的事件。 思考下面的伪代码,提交异步传输然后等待完成。这是一种在异步接口之上实现同步接口的方法 (尽管libusb的方式比这个页面上的更高级,但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]; int completed = 0;
transfer = libusb_alloc_transfer(0); libusb_fill_control_setup(buffer, LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_ENDPOINT_OUT, 0x04, 0x01, 0, 0); libusb_fill_control_transfer(transfer, dev, buffer, cb, &completed, 1000); libusb_submit_transfer(transfer);
while (!completed) { poll(libusb file descriptors, 120*1000); if (poll indicates activity) libusb_handle_events_timeout(ctx, 0); } printf("completed!"); // other code here } 此处是针对一种条件的异步事件的序列化完成,这种条件是特定的传输完成。poll()循环设置了一个很长的超时以便于在没有任何事情发生的情况下(在某些原因下不被限制)最小化CPU利用率。 如果这是唯一的使用libusb的文件描述符的线程,这没有问题: 另一个线程将吞没我们感兴趣的事件是没有危险的。另一方面,如果另一个线程使用相同等我文件描述符,这将有可能它将接收到我们感兴趣的事件。在这种情况下, myfunc()在循环下次检测的时候,直到120秒之后将只能发现传输已经完成。显然2分钟的延时是让人无法忍受的,更不要想通过使用很短延时来解决这个问题。 解决方案是去报没有2个线程同时使用一个文件描述符。一个幼稚的实现将影响库的性能, 所以libusb提供了下面的文档化的方案来保证没有性能方面的损失。 在我们继续深入之前,值得提一下的是所有的libusb封装的事件处理程序都完全支持下面文档的方案,这包括libusb_handle_events()所有的同步I/O 函数——libusb为你把这个头疼的事情隐藏了起来。如果你坚持那种级别,你不需要担心任何问题。 问题是当我们面对libusb 暴露出文件描述符来允许你将异步USB I/O融入主循环的事实, 有效地允许你在libusb的后面做些工作。如果你自己用libusb的文件描述符,并将它们传递给poll()/select()时 ,你需要注意相关问题。 事件锁 第一个被介绍的概念是事件锁。事件锁用于序列化想要处理事件的线程,以便于在任意时间只有唯一的一个线程正在处理事件。 你必须在使用libusb文件描述符之前使用事件锁函数 libusb_lock_events()。一旦你离开poll()/select()循环,你必须立即使用函数 libusb_unlock_events()释放事件锁。 让其他线程为你工作 虽然事件锁是解决方案的一个重要部分,但仅用它本身是不够的,如果下面的情况发生你也许会惊讶... libusb_lock_events(ctx); while (!completed) { poll(libusb file descriptors, 120*1000); if (poll indicates activity) libusb_handle_events_timeout(ctx, 0); } libusb_unlock_events(ctx); ...答案是这个是不对的。这是因为代码中显示的传输也许会话更长的时间(比如说30秒)完成。 直到传出完成之前锁都不会被释放。 另一个与之类似想要处理事件的代码的线程也许正在处理一个传输,并且将在几毫秒之后完成。由于锁的独占,尽管有着如此之快的完成时间,另一个线程不能检测到它的这种传输状态,直到上面的代码完成(30秒之后) 。 为了解决这个问题, libusb为你提供了一种方法决定什么时候另一个线程正在处理事件。 它也提供了一种方法锁住你的线程直到事件处理线程完成一个事件(并且这种机制并不需要使用文件描述符)。 在确定另一个线程正在处理事件,你通过libusb_lock_event_waiters()使用获得一个事件等待锁,然后你重新检测是否其他线程仍在处理事件。如果是这样,你可以调用libusb_wait_for_event()。 libusb_wait_for_event() 将你的程序转到休眠状态直到事件发生。或者知道一个线程释放这个事件锁。当这些事件发生或者你的线程被唤醒,你需要重新检测它正在等待的条件。同样需要重新检测是否另一个线程在处理事件,如果没有,它应该自己开始处理事件。这应该看起来像下面的伪代码: retry: if (libusb_try_lock_events(ctx) == 0) { // we obtained the event lock: do our own event handling while (!completed) { if (!libusb_event_handling_ok(ctx)) { libusb_unlock_events(ctx); goto retry; } poll(libusb file descriptors, 120*1000); if (poll indicates activity) libusb_handle_events_locked(ctx, 0); } libusb_unlock_events(ctx); } else { // another thread is doing event handling. wait for it to signal us that // an event has completed libusb_lock_event_waiters(ctx);
while (!completed) { // now that we have the event waiters lock, double check that another // thread is still handling events for us. (it may have ceased handling // events in the time it took us to reach this point) if (!libusb_event_handler_active(ctx)) { // whoever was handling events is no longer doing so, try again libusb_unlock_event_waiters(ctx); goto retry; }
libusb_wait_for_event(ctx); } libusb_unlock_event_waiters(ctx); } printf("completed!\n"); 一个天真的人看到上面的代码也许会建议这个只能支持一个事件等待者 (因此总工2个竞争的线程,另一个处理事件),因为当等待事件的时候,事件等待者看起来已经事件等待锁。但是,系统支持多个事件等待者,因为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. 在初始化的时候,libusb打开一个内部管道,然后增加管道的读端到要使用的文件描述符集合中。 2. 在调用libusb_close()的时候,libusb在这个控制管道上写一些复制下来的数据。这回立即中断事件处理程序。libusb也会在内部为这个高级别事件记录下来它正在尝试中断事件处理程序。 3. 在这时,上面的一些函数开始不同的行为: o libusb_event_handling_ok() 开始返回1,表示事件处理是不能继续的。 o libusb_try_lock_events() 开始返回1,表示另一个线程持有事件处理锁,即使锁没有被占用。 o libusb_event_handler_active()开始返回1,表示另一个线程正在处理事件,即使并有处理。 4. 上面的在事件处理停止的处理结果中发生改变。并迅速的放弃锁,给高级别的libusb_close() 操作一个"便利"去获取事件锁。所有争夺处理事件的线程都变成事件等待者。 5. 在 libusb_close()持有事件锁,libusb可以从轮询集合中安全的删除文件描述符,in the safety of knowledge that 没有任何线程正在轮询那些描述符或者正尝试访问轮询集合。 6. 在获取事件锁后,关闭操作快速完成(通常几毫秒) 然后直接释放事件锁。 7. 同时,libusb_event_handling_ok() 执行,并且其他的都返回都原始状态,文档描述的行为。 8. 事件锁的释放会唤醒所有正在等待事件的线程,然后开始竞争再次成为事件处理程序。他们中的一个将会成功;它将重新获得轮询描述符的列表,然后USB I/O将会正常执行下去。 libusb_open()是相似的,而且是一个更相似的例子,当调用libusb_open()的时候: 1. 设备是被打开的,并且文件描述符是被加入到轮询集合的。 2. libusb发送一些复制数据到控制管道,然后记录它正在尝试修改轮询描述符集合。 3. 事件处理程序被中断,就如同libusb_close()的效果一样,发生相同的行为改变,引起所有的事件处理线程编程事件等待者。 4. libusb_open() 实现使用它的免费权利获得事件锁。 5. 很顺利的它暂停了事件处理程序,libusb_open()释放事件锁。 6. 事件等待者线程被唤醒,然后再次争夺成为事件处理程序。其中一个再次成功获取包含新增设备的轮询描述符列表。 结束语 上面的内容也许看起来有点复杂,但是我希望我已经讲明白为什么这么复杂是必要的。同样,不要忘记这只应用于那些将使用libusb的文件描述符和集成它们到自己的轮询循环中的应用程序。 你也许认为在你的多线程应用程序中忽略一些上面详细说到的规则和锁是没有问题的,因为你不认为2个线程会在同一时刻轮询一个描述符。如果是这种原因,那么这对你是个好消息,因为你不需要担心。但是请注意这里,记住同步I/O函数在内部处理事件。如果你有一个线程在循环中处理事件 (没有实现上述文档所说的规则和锁的概念) ,并且另一个线程正在尝试发送一个同步USB传输,将发生2个线程监视同一描述符的结果,并且会发生上述不希望发生的问题。解决法案就是让你的轮询线程遵循规则,同步I/O函数也是这么做的,并且这将使线程之间处理的更协调。 如果你已经有一个用于处理事件的线程,它占有事件锁很长时间是完全合理的。任何你从其他线程调用的同步I/O函数都会显而易见的退回到上面详细说到的“事件等待者”的机制状态下。你的事件处理线程唯一需要考虑做的就是与libusb_event_handling_ok()有关的事情:你必须在每个poll()之前调用它,如果已经被使用你必须放弃事件锁。 |
|