在Linux上等待多个条件变量而不需要不必要的休眠?

29

我正在编写一个延迟敏感的应用程序,实际上希望能够同时等待多个条件变量。我以前读过在Linux上获得此功能的几种方法(显然Windows内置了这个功能),但对于我的应用程序似乎都不适用。我知道的方法有:

  1. 让一个线程等待您想要等待的每个条件变量,当唤醒时,会发出信号一个单一的条件变量,您将在其中等待。

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

  3. 而是编写虚拟字节到文件或管道中,并对其进行轮询。

#1 & #2 都不合适,因为它们导致不必要的睡眠。对于#1,您必须等待虚拟线程唤醒,然后发出信号给真正的线程,然后等待真正的线程唤醒,而不是真正的线程直接唤醒 -- 在这上面额外的调度器量子实际上对我的应用程序很重要,我宁愿不使用完整的RTOS。对于#2,更糟糕的是,你可能会花费 N * 超时时间来休眠,或者你的超时时间为 0,这样你永远不会睡觉(无休止地烧掉 CPU 并饿死其他线程也是不好的)。

#3来说,管道存在问题,因为如果正在“发出信号”的线程很忙甚至崩溃(实际上我在处理单独的进程而不是线程 -- 互斥锁和条件将存储在共享内存中),则写入线程将被卡住,因为管道的缓冲区将被填满,所有其他客户端也将如此。文件存在问题,因为随着应用程序运行时间越长,它将无限增长。

是否有更好的方法来做到这一点?对于Solaris同样适用的答案感兴趣。


1
我在C++0x线程原语中也遇到了这个限制,这似乎是基于pthread的最低公共分母。 - Marsh Ray
1
你不能只使用一个信号量吗?一旦等待的线程获得一个单元,它可以轮询各个源以找到已经“触发”的源(也许是一个易失性布尔数组?)。 - Martin James
4个回答

17
您的第三个选项(向文件或管道中写入虚拟字节,并对其进行轮询)在Linux上有更好的选择:eventfd
与管道中的有限大小缓冲区或文件中的无限增长缓冲区不同,使用eventfd可以获得一个内核中的无符号64位计数器。8字节的write将数字添加到计数器中;8字节的read将计数器清零并返回其先前值(没有EFD_SEMAPHORE),或者将计数器减1并返回1(有EFD_SEMAPHORE)。当计数器为非零值时,文件描述符被认为是可读的,以供轮询函数(selectpollepoll)使用。
即使计数器接近64位限制,如果您将文件描述符设置为非阻塞状态,则write仅会以EAGAIN失败。当计数器为零时,read也会发生相同的情况。

太棒了,这有很多用例。 - Joseph Garvin
很遗憾,eventfd不允许“命名”。也就是说,您不能在没有父子关系的进程之间使用eventfd(或任何其他* fd机制)来共享句柄。否则,这些机制将完全取代现有的过时、功能不足、性能低效的POSIX命名垃圾(做类似的事情),但不能与其他fd等待混合使用或由epoll / poll / select等待。 - Michael Goldshteyn
1
@Michael Goldshteyn:你不能使用Unix域套接字通过文件描述符传递将eventfd描述符(或任何其他文件描述符)传递给不相关的进程吗? - CesarB
@CesarB,是的,那是一个选项,但它增加了很多复杂性,而本应该是一个简单功能:基于命名fd的事件和定时器。 - Michael Goldshteyn
然而,即使它被“信号化”,每次读取都会导致一次往返到内核。与信号量相比,这不是最好的方法。 - xryl669
我在哪里可以找到使用eventfd来信号pthread条件变量的示例?谢谢。 - Frank

14

如果你在谈论POSIX线程,我建议使用单个条件变量和事件标志数或类似的东西。思路是使用对等的condvar互斥体来保护事件通知。无论如何,在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 (copy & FLAG_EVENT_1)
            {
                printf("EVENT 1\n");
                copy ^= FLAG_EVENT_1;
            }

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

                // And let EVENT 2 to be 'quit' signal.
                // 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;
}

1
不要复制事件标志,而是可以执行任何事件列表的“弹出”操作。除非您释放了mtx(当然,仅在任何修改都在同一互斥体下时才这样做),否则列表不会改变。这是此方案的最大优点之一。例如,您可以使用事件队列,是的,此队列受到保护。当“读取器”检查它并“弹出”时,任何想要“推送”的人都将等待短时间。但请注意,您不应在锁定状态下处理事件,只能进行“提取”。 - Roman Nikitchenko
我最终没有使用这个答案,因为它对我的应用程序来说还不太合适,但就问题中提供的信息而言,这是正确的方法 :) - Joseph Garvin
1
在处理收到的事件后,这个答案不是缺少了pthread_mutex_lock(&mtx);吗? - jotik
@jotik 是的,你说得对。答案是作为一个概念写的。原始代码与事件队列有点不同,而不仅仅是标志。 - Roman Nikitchenko
如果你不使用它,复制的意义何在呢?也许你本来想复制并解锁互斥锁,然后对该副本执行操作? - Martin
显示剩余2条评论

6
如果您想在 POSIX 条件变量模型下获得最大的灵活性,必须避免编写仅通过公开条件变量向其用户通信事件的模块。(这样您本质上就重新发明了一个信号量。)
应该设计活动模块,使它们的接口通过注册函数提供回调通知事件,并且如果需要,可以注册多个回调。
多个模块的客户端向每个模块注册回调。这些回调可以全部路由到一个共同的地方,在那里它们锁定相同的互斥体、更改一些状态、解锁并触发相同的条件变量。
此设计还提供了这样的可能性:如果响应事件所做的工作量相当小,那么也许可以在回调的上下文中完成。
回调在调试方面也具有一些优点。您可以在以回调形式到达的事件上设置断点,并查看生成该事件的调用堆栈。如果在通过某种消息传递机制或信号量唤醒时设置断点,则调用跟踪不会显示事件的起源。
话虽如此,您可以使用互斥锁和条件变量创建支持等待多个对象的同步原语。这些同步原语可以在内部基于回调实现,而应用程序的其余部分对此是不可见的。
简而言之,对于每个线程想要等待的对象,等待操作都会将一个回调接口排队到该对象。当对象被触发时,它会调用所有已注册的回调。唤醒的线程将出队所有回调接口,并查看每个接口中的某些状态标志,以查看哪些对象被触发。

2

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接