在Linux上等待多个条件变量而没有不必要的睡眠?

时间:2010-05-17 14:57:13

标签: c multithreading pthreads conditional-statements scheduling

我正在编写一个对延迟敏感的应用,它实际上想要一次等待多个条件变量。我之前已经阅读了几种在Linux上获得此功能的方法(显然这是在Windows上构建的),但它们似乎都不适合我的应用程序。我所知道的方法是:

  1. 让一个线程等待你想要等待的每个条件变量,当你被唤醒时,它会发出一个你等待的单个条件变量。

  2. 通过定时等待循环遍历多个条件变量。

  3. 将虚拟字节写入文件或管道,然后轮询这些字节。

  4. #1& #2不合适,因为它们会造成不必要的睡眠。使用#1,你必须等待虚拟线程唤醒,然后发出真实线程的信号,然后唤醒真正的线程,而不是真正的线程刚刚开始唤醒 - 额外的调度程序量子花费在这实际上对我的应用程序很重要,我宁愿不必使用完整的RTOS。 #2更糟糕的是,你可能会花费N *超时时间睡眠,或者你的超时时间为0,在这种情况下你永远不会睡觉(无休止地烧掉CPU并使其他线程挨饿也是坏事)。

    对于#3,管道是有问题的,因为如果“发出信号”的线程正忙或甚至崩溃(我实际上处理的是单独的进程而不是线程 - 互斥和条件将存储在共享内存中),然后写入线程将被卡住,因为管道的缓冲区将是满的,任何其他客户端也是如此。文件存在问题,因为应用程序运行的时间越长,它就会无休止地增长。

    有更好的方法吗?对于适用于Solaris的答案感到好奇。

4 个答案:

答案 0 :(得分:15)

您的#3选项(将虚拟字节写入文件或管道,并对其进行轮询)在Linux上有更好的选择:eventfd

使用eventfd而不是有限大小的缓冲区(如在管道中)或无限增长的缓冲区(如在文件中),您有一个内核无符号64位计数器。一个8字节write向计数器添加一个数字;一个8字节的read将计数器归零并返回其先前的值(不带EFD_SEMAPHORE),或者将计数器递减1并返回1(带EFD_SEMAPHORE)。当计数器非零时,文件描述符被认为对轮询函数(selectpollepoll)可读。

即使计数器接近64位限制,如果您使文件描述符不阻塞,write也只会失败EAGAIN。当计数器为零时,read也会发生同样的情况。

答案 1 :(得分:11)

如果你在谈论POSIX线程,我建议使用单个条件变量和事件标志的数量或类似的东西。我们的想法是使用peer condvar mutex来保护事件通知。无论如何,你需要在cond_wait()退出后检查事件。这是我足够的代码,可以从我的训练中说明这一点(是的,我检查过它的运行情况,但请注意它是在前一段时间准备的,并且是为了新手而匆忙)。

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

static pthread_cond_t var;
static pthread_mutex_t mtx;

unsigned event_flags = 0;
#define FLAG_EVENT_1    1
#define FLAG_EVENT_2    2

void signal_1()
{
    pthread_mutex_lock(&mtx);
    event_flags |= FLAG_EVENT_1;
    pthread_cond_signal(&var);
    pthread_mutex_unlock(&mtx);
}

void signal_2()
{
    pthread_mutex_lock(&mtx);
    event_flags |= FLAG_EVENT_2;
    pthread_cond_signal(&var);
    pthread_mutex_unlock(&mtx);
}

void* handler(void*)
{
    // Mutex is unlocked only when we wait or process received events.
    pthread_mutex_lock(&mtx);

    // Here should be race-condition prevention in real code.

    while(1)
    {
        if (event_flags)
        {
            unsigned copy = event_flags;

            // We unlock mutex while we are processing received events.
            pthread_mutex_unlock(&mtx);

            if (event_flags & FLAG_EVENT_1)
            {
                printf("EVENT 1\n");
                event_flags ^= FLAG_EVENT_1;
            }

            if (event_flags & FLAG_EVENT_2)
            {
                printf("EVENT 2\n");
                event_flags ^= FLAG_EVENT_2;

                // And let EVENT 2 is signal to close.
                // In this case for consistency we break with locked mutex.
                pthread_mutex_lock(&mtx);
                break;
            }

            // Note we should have mutex locked at the iteration end.
            pthread_mutex_lock(&mtx);
        }
        else
        {
            // Mutex is locked. It is unlocked while we are waiting.
            pthread_cond_wait(&var, &mtx);
            // Mutex is locked.
        }
    }

    // ... as we are dying.
    pthread_mutex_unlock(&mtx);
}

int main()
{
    pthread_mutex_init(&mtx, NULL);
    pthread_cond_init(&var, NULL);

    pthread_t id;
    pthread_create(&id, NULL, handler, NULL);
    sleep(1);

    signal_1();
    sleep(1);
    signal_1();
    sleep(1);
    signal_2();
    sleep(1);

    pthread_join(id, NULL);
    return 0;
}

答案 2 :(得分:3)

如果您希望在POSIX条件变量同步模型下具有最大的灵活性,则必须避免编写仅通过公开条件变量来向其用户传达事件的模块。 (然后你基本上重新发明了一个信号量。)

应设计活动模块,使其接口通过注册函数提供事件的回调通知:如有必要,可以注册多个回调。

多个模块的客户端为每个模块注册一个回调。这些都可以被路由到一个公共的地方,在那里他们锁定相同的互斥锁,改变一些状态,解锁并命中相同的条件变量。

这种设计还提供了这样的可能性:如果响应事件而完成的工作量相当小,也许它可以在回调的上下文中完成。

回调在调试方面也有一些优势。您可以在以回调形式到达的事件上放置断点,并查看它是如何生成的调用堆栈。如果对作为信号量唤醒到达的事件或通过某种消息传递机制设置断点,则调用跟踪不会显示事件的来源。


话虽如此,您可以使用互斥锁和条件变量创建自己的同步原语,这些变量支持等待多个对象。这些同步原语可以在内部基于回调,其方式对应用程序的其余部分是不可见的。

它的要点是,对于线程想要等待的每个对象,等待操作会使用该对象对回调接口进行排队。当对象发出信号时,它会调用所有已注册的回调。唤醒的线程将所有回调接口出列,并查看每个回调接口中的一些状态标志,以查看哪些对象发出信号。

答案 3 :(得分:2)

对于等待多个条件变量,有一个Solaris实现,如果您感兴趣,可以移植到Linux:WaitFor API