Ждать несколько условных переменных в Linux без лишних снов? - PullRequest
26 голосов
/ 17 мая 2010

Я пишу приложение, чувствительное к задержке, которое по сути хочет ожидать нескольких переменных условия одновременно. Ранее я читал несколько способов получить эту функциональность в Linux (очевидно, она встроена в Windows), но ни один из них не подходит для моего приложения. Методы, которые я знаю:

  1. Пусть по одному потоку будет ожидаться каждая из условных переменных, которые вы хотите ждать, что при пробуждении будет сигнализировать об одной условной переменной, которую вы ожидаете вместо этого.

  2. Циклическое переключение нескольких условных переменных с выдержкой времени.

  3. Вместо этого записываются фиктивные байты в файлы или каналы и опрашивают их.

# 1 & # 2 не подходят, потому что они вызывают ненужный сон. При использовании # 1 вам нужно дождаться, чтобы фиктивный поток проснулся, затем дать сигнал реальному потоку, а затем, чтобы реальный поток проснулся, а не реальный поток, который просто просыпается для начала - дополнительный квант планировщика, потраченный на это действительно важно для моего приложения, и я бы предпочел не использовать полноценную ОСРВ. # 2 еще хуже, вы потенциально тратите N * тайм-аут на время ожидания, или ваш тайм-аут будет равен 0, и в этом случае вы никогда не спите (бесконечное горение процессора и голодание других потоков также плохо).

Для # 3 каналы проблематичны, потому что, если «сигнализируемый» поток занят или даже падает (на самом деле я имею дело с отдельным процессом, а не с потоками - мьютексы и условия будут храниться в общей памяти), тогда поток записи будет зависать, потому что буфер канала будет заполнен, как и любые другие клиенты. Файлы проблематичны, потому что они будут расти бесконечно дольше, чем будет работать приложение.

Есть ли лучший способ сделать это? Любопытны ответы, подходящие и для Solaris.

Ответы [ 4 ]

15 голосов
/ 02 октября 2011

Ваш вариант №3 (вместо этого записывается фиктивные байты в файлы или каналы и опрашивает их) в Linux лучше: eventfd.

Вместо буфера ограниченного размера (как в канале) или бесконечно растущего буфера (как в файле), с eventfd у вас есть 64-битный счетчик без знака в ядре. 8-байтовый write добавляет число к счетчику; 8-байтовый read либо обнуляет счетчик и возвращает его предыдущее значение (без EFD_SEMAPHORE), либо уменьшает счетчик на 1 и возвращает 1 (с EFD_SEMAPHORE). Дескриптор файла считается читаемым для функций опроса (select, poll, epoll), когда счетчик ненулевой.

Даже если счетчик близок к 64-битному пределу, write просто не будет работать с EAGAIN, если вы сделали дескриптор файла неблокирующим. То же самое происходит с read, когда счетчик равен нулю.

11 голосов
/ 20 мая 2010

Если вы говорите о потоках POSIX, я бы рекомендовал использовать одну переменную условия и число флагов событий или что-то подобное. Идея состоит в том, чтобы использовать мьютекс peer 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 (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;
}
3 голосов
/ 24 апреля 2014

Если вы хотите максимальной гибкости в модели синхронизации условных переменных POSIX, вы должны избегать написания модулей, которые передают события своим пользователям только посредством предоставления условной переменной. (Затем вы, по сути, заново изобрели семафор.)

Активные модули должны быть спроектированы таким образом, чтобы их интерфейсы обеспечивали уведомления о событиях обратного вызова через зарегистрированные функции: и, при необходимости, чтобы можно было зарегистрировать несколько обратных вызовов.

Клиент нескольких модулей регистрирует обратный вызов с каждым из них. Все они могут быть направлены в обычное место, где они блокируют один и тот же мьютекс, изменяют некоторое состояние, разблокируют и нажимают на одну и ту же переменную условия.

Этот дизайн также дает возможность того, что, если объем работы, выполненной в ответ на событие, достаточно мал, возможно, это можно сделать просто в контексте обратного вызова.

Обратные вызовы также имеют некоторые преимущества при отладке. Вы можете поставить точку останова на событие, которое приходит в форме обратного вызова, и посмотреть, как оно было сгенерировано. Если вы ставите точку останова на событие, которое приходит как пробуждение семафора, или с помощью какого-либо механизма передачи сообщений, трассировка вызова не раскрывает источник события.


При этом вы можете создавать свои собственные примитивы синхронизации с мьютексами и условными переменными, которые поддерживают ожидание нескольких объектов. Эти примитивы синхронизации могут быть внутренне основаны на обратных вызовах способом, который невидим для остальной части приложения.

Суть в том, что для каждого объекта, которого поток хочет ждать, операция ожидания ставит в очередь интерфейс обратного вызова с этим объектом. Когда объект получает сигнал, он вызывает все свои зарегистрированные обратные вызовы. Пробужденные потоки исключают из очереди все интерфейсы обратного вызова и просматривают некоторые флаги состояния в каждом из них, чтобы увидеть, какие объекты сигнализируют.

2 голосов
/ 26 апреля 2011

Для ожидания нескольких условных переменных существует реализация Solaris, которую вы можете перенести на Linux, если вам интересно: WaitFor API

...