此文章使用Goolge进行翻译学习使用。原文网址为:http://libusb.sourceforge.net/api-1.0/mtasync.html
本文章是为了调查:(libusb_bulk_transfer()
会阻塞,阻塞时间长达60s的问题)[http://blog.csdn.net/encourage2011/article/details/78760276]。
libusb是一个线程安全的库,但是应用于与多线程中的libusb交互的应用程序必须要注意。
必须解决的根本问题是,所有的libusb I/O 都围绕着通过poll()/select()系统调用来监视文件描述符。这是直接暴露在异步接口,但需要着重注意的是:同步接口是在异步接口的顶部实现,因此同样的考虑适用。
问题是,如果两个或多个线程同时对libusb的文件描述符调用poll()或select(),那么当事件到达时,只有其中一个线程会被唤醒,另一个线程将完全没有意识到发生了什么事情。
考虑下面的伪代码,它提交一个异步传输,然后等待它完成。 这种风格是你可以在异步接口之上实现一个同步接口的一种方式(而且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_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, &zero_tv);
}
printf("completed!");
// other code here
}
在这里,我们正在序列化一个异步事件的完成情况与某一条件相反,该条件是完成一个特定的传输。
poll()
循环具有很长的超时时间,以便在没有任何事情发生的情况下(可以合理地无限制)最小化CPU使用率。
如果这是轮询libusb的文件描述符的唯一线程,则没有问题:另一个线程将不会吞下我们感兴趣的事件。另一方面,如果有另一个线程轮询相同的描述符 ,有可能会收到我们感兴趣的事件。在这种情况下,myfunc()
只会意识到,在循环的下一次迭代中,传输已经完成,最多120秒后。
显然,两分钟的延迟是不可取的,甚至不能考虑使用短暂的超时来解决这个问题!
这里的解决方案是确保没有两个线程同时轮询文件描述符。一个原始的执行会影响库的能力,所以libusb提供下面的方案以确保不丢失功能。
在我们进一步讨论之前,值得一提的是,所有libusb包装的事件处理程序都完全遵循下文所述的方案。 这包括libusb_handle_events()
及其相关函数以及所有同步I/O函数。libusb已把用户的头痛的事隐藏起来。
libusb_handle_events()
即使只使用libusb_handle_events()
和同步I/O函数,仍然可能有竞争条件。 你可能会试图用libusb_handle_events()
来解决上述问题:
libusb_submit_transfer(transfer);
while (!completed) {
libusb_handle_events(ctx);
}
printf("completed!");
然而,完成检查和libusb_handle_events()
获取事件锁之间存在竞争,所以另一个线程可能已经完成了传输,导致该线程挂起,直到发生超时或其他事件。 另请参见提交6696512aade99bb15d6792af90ae329af270eba6
,它在libusb的同步API实现中修复了这个问题。
修正这个竞争需要检查变量完成后才采取事件锁定,这打破了只是调用libusb_handle_events()
的概念,而不必担心锁定。 这就是为什么libusb-1.0.9引入了新的
libusb_handle_events_timeout_completed()
和libusb_handle_events_completed()
函数,它们在获取锁之后处理完成检查:
libusb_submit_transfer(transfer);
while (!completed) {
libusb_handle_events_completed(ctx, &completed);
}
printf("completed!");
这很好地解决了我们的例子中的竞争。 请注意,如果您只想提交一个传输并等待完成,那么使用其中一个同步I/O函数就容易多了。
问题是,当我们考虑到libusb公开文件描述符以允许将异步USB I / O集成到现有的主循环这一事实时,可以有效地让你在libusb的背后做一些工作。 如果你使用libusb的文件描述符并将它们传递给poll()/ select()
,你需要知道有可能出现相关的问题。
首先要介绍的概念是事件锁。 事件锁用于序列化要处理事件的线程,以便一次只有一个线程处理事件。
在轮询libusb文件描述符之前,必须使用libusb_lock_events()
来锁定事件锁定。 您必须在使用libusb_unlock_events()
中止poll()/select()
循环后立即释放锁。
尽管事件锁是解决方案的重要组成部分,但仅靠个人解决方案还不够。 你可能想知道下面是否足够…
libusb_lock_events(ctx);
while (!completed) {
poll(libusb file descriptors, 120*1000);
if (poll indicates activity)
libusb_handle_events_timeout(ctx, &zero_tv);
}
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, NULL);
}
libusb_unlock_event_waiters(ctx);
}
printf("completed!\n");
上面代码的看来可能表明,这只能支持一个事件等待器(因此总共有2个竞争线程,另一个做事件处理),因为事件等待器似乎已经获取到事件等待锁等待事件触发。 但是,系统确实支持多个事件等待器,因为libusb_wait_for_event()
实际上在等待时放下了锁,并在继续之前再次获取它。
我们现在已经实现了可以动态处理没有人处理事件的情况的代码(所以我们应该自己动手实现它),还可以处理另一个线程正在进行事件处理的情况(所以我们可以搭载它们)。 它也可以处理两者的组合,例如,另一个线程正在进行事件处理,但是无论出于何种原因,在满足条件之前停止这样做,所以我们接管事件处理。
上述伪代码中引入了四个函数。 他们的重要性应该从上面显示的代码中显而易见。
libusb_try_lock_events ()是一个非阻塞函数,它试图获取事件锁定,但是如果它被竞争则返回失败代码。
libusb_event_handling_ok()
检查你的线程是否正在执行事件处理。有时候,libusb需要中断事件处理程序,这是如何检查你是否被中断的方法。如果这个函数返回0,正确的行为是让你放弃事件处理锁,然后重复这个循环。下面的libusb_try_lock_events()
会失败,所以你会成为一个事件等待器。欲了解更多信息,请阅读下面的全文。
libusb_handle_events_locked()
是libusb_handle_events_timeout()
的变体,你可以在保持事件锁定的情况下调用它。libusb_handle_events_timeout()
本身实现了和上面类似的逻辑,所以当你“在libusb的后面工作”的时候一定不要调用它,就像这里的情况一样。
libusb_event_handler_active()
确定是否有人正在持有事件锁。
你可能想知道为什么没有函数来唤醒在libusb_wait_for_event()
上被阻塞的所有线程。这是因为libusb可以在内部执行这个操作:当有人调用libusb_unlock_events()
或传输完成时(在回调完成后),它会唤醒所有这样的线程。