Подождите несколько переменных условия на Linux без лишних снов?
Я пишу приложение, чувствительное к задержкам, которое фактически хочет ждать сразу нескольких переменных условий. Я читал ранее несколько способов получить эту функциональность в Linux (по-видимому, это встроено в Windows), но ни один из них не подходит для моего приложения. Методы, о которых я знаю, следующие:
-
Подождите один поток на каждой переменной состояния, которую вы хотите подождать, которая, когда woken будет сигнализировать одну переменную условия, которую вы ждете, вместо этого.
-
Переключение между несколькими переменными условия с ожидаемым временем ожидания.
-
Вместо этого создайте фиктивные байты для файлов или труб и опросите их.
# 1 и # 2 не подходят, потому что они вызывают ненужный спать. С# 1 вам нужно дождаться, когда поток фиктивного типа проснется, а затем сообщит реальный поток, а затем, чтобы реальный поток проснулся, вместо реального потока, который просто просыпался для начала - дополнительный квант планировщика, потраченный на это действительно важно для моего приложения, и я бы предпочел не использовать полноценную RTOS. # 2 еще хуже, вы потенциально тратите время ожидания N *, или ваш таймаут будет 0, и в этом случае вы никогда не будете спать (бесконечно горящий процессор и голодные другие потоки также плохие).
Для №3 трубы являются проблематичными, потому что если поток "сигнализирован" занят или даже падает (я на самом деле разбираюсь с отдельным процессом, а не с потоками), мьютексы и условия будут храниться в общей памяти) то записывающий поток будет застревать, потому что буфер буфера будет заполнен, как и любые другие клиенты. Файлы проблематичны, потому что вы будете бесконечно расти, чем дольше работает приложение.
Есть ли лучший способ сделать это? Любопытно, что ответы также подходят для Solaris.
Ответы
Ответ 1
Если вы говорите о потоках 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;
}
Ответ 2
Ваш вариант №3 (вместо написания фиктивных байтов для файлов или каналов и опроса на них) имеет лучшую альтернативу в Linux: eventfd
.
Вместо буфера ограниченного размера (как в трубе) или бесконечно растущего буфера (как в файле) с eventfd
у вас есть 64-разрядный счетчик без знака в ядре. 8-байтовый write
добавляет число к счетчику; 8-байтовый read
либо обнуляет счетчик и возвращает его предыдущее значение (без EFD_SEMAPHORE
), либо уменьшает счетчик на 1 и возвращает 1 (с EFD_SEMAPHORE
). Дескриптор файла считается читаемым для функций опроса (select
, poll
, epoll
), когда счетчик отличен от нуля.
Даже если счетчик близок к 64-битовому пределу, write
будет просто терпеть неудачу с EAGAIN
, если вы сделаете дескриптор файла неблокирующимся. То же самое происходит с read
, когда счетчик равен нулю.
Ответ 3
Для ожидания нескольких переменных условий для Solaris существует реализация, которую вы могли бы подключить к Linux, если вам интересно: WaitFor API
Ответ 4
Если вам нужна максимальная гибкость в рамках модели переменных состояния условия POSIX, вы должны избегать писать модули, которые сообщают события своим пользователям, только посредством отображения переменной условия. (Тогда вы, в сущности, заново изобрели семафор.)
Активные модули должны быть сконструированы таким образом, чтобы их интерфейсы обеспечивали обратные вызовы уведомлений о событиях через зарегистрированные функции: и, при необходимости, чтобы можно было зарегистрировать несколько обратных вызовов.
Клиент из нескольких модулей регистрирует обратный вызов с каждым из них. Все они могут быть направлены в обычное место, где они блокируют один и тот же мьютекс, меняют состояние, разблокируют и попадают в ту же переменную условия.
Эта конструкция также предлагает возможность того, что если объем выполненной работы в ответ на событие достаточно мал, возможно, это может быть сделано только в контексте обратного вызова.
Обратные вызовы также имеют некоторые преимущества при отладке. Вы можете поместить точку останова на событие, которое приходит в виде обратного вызова, и посмотреть стек вызовов, как он был сгенерирован. Если вы поместите контрольную точку на событие, которое приходит в качестве пробуждения семафора или через какой-либо механизм передачи сообщений, трассировка вызова не показывает происхождение события.
При этом вы можете создавать свои собственные примитивы синхронизации с мьютексами и переменными условия, которые поддерживают ожидание на нескольких объектах. Эти примитивы синхронизации могут быть внутренне основаны на обратных вызовах, таким образом, который невидим для остальной части приложения.
Суть его в том, что для каждого объекта, который хочет ожидать поток, операция ожидания приостанавливает интерфейс обратного вызова с этим объектом. Когда объект сигнализируется, он вызывает все зарегистрированные обратные вызовы. Проснувшиеся потоки деактивируют все интерфейсы обратного вызова и просматривают некоторые флаги состояния в каждом из них, чтобы видеть, какие объекты сигнализированы.