Multi-threaded applications and asynchronous I/O(翻译)

来源:互联网 发布:amd优化档案 编辑:程序博客网 时间:2024/06/03 14:52

此文章使用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()或传输完成时(在回调完成后),它会唤醒所有这样的线程。