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

Jos*_*vin 27 c multithreading scheduling pthreads conditional-statements

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

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

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

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

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

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

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

Ces*_*arB 15

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

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

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


Rom*_*nko 12

如果你在谈论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 (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;
}
Run Code Online (Sandbox Code Playgroud)


Kaz*_*Kaz 5

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

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

多个模块的客户端向每个模块注册一个回调。这些都可以路由到一个公共位置,在那里它们锁定相同的互斥锁、更改某些状态、解锁并命中相同的条件变量。

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

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


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

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