#c #linux #timer #pthreads #cancellation
#c #linux #таймер #pthreads #отмена
Вопрос:
Я создал псевдокласс Timer на языке C, который имеет возможность обратного вызова и может быть отменен. Я родом из мира .NET / C #, где все это делается с помощью фреймворка, и я не эксперт в pthreads.
В .NET есть токены отмены, которые вы можете подождать, что означает, что мне не нужно так сильно беспокоиться о гайках и болтах.
Однако использование pthreads немного более низкого уровня, чем я привык, поэтому мой вопрос:
Есть ли какие-либо проблемы с тем, как я это реализовал?
Спасибо за любые комментарии, которые у вас могут быть.
Структура таймера:
typedef struct _timer
{
pthread_cond_t Condition;
pthread_mutex_t ConditionMutex;
bool IsRunning;
pthread_mutex_t StateMutex;
pthread_t Thread;
int TimeoutMicroseconds;
void * Context;
void (*Callback)(bool isCancelled, void * context);
} TimerObject, *Timer;
Модуль C:
static void *
TimerTask(Timer timer)
{
struct timespec timespec;
struct timeval now;
int returnValue = 0;
clock_gettime(CLOCK_REALTIME, amp;timespec);
timespec.tv_sec = timer->TimeoutMicroseconds / 1000000;
timespec.tv_nsec = (timer->TimeoutMicroseconds % 1000000) * 1000000;
pthread_mutex_lock(amp;timer->StateMutex);
timer->IsRunning = true;
pthread_mutex_unlock(amp;timer->StateMutex);
pthread_mutex_lock(amp;timer->ConditionMutex);
returnValue = pthread_cond_timedwait(amp;timer->Condition, amp;timer->ConditionMutex, amp;timespec);
pthread_mutex_unlock(amp;timer->ConditionMutex);
if (timer->Callback != NULL)
{
(*timer->Callback)(returnValue != ETIMEDOUT, timer->Context);
}
pthread_mutex_lock(amp;timer->StateMutex);
timer->IsRunning = false;
pthread_mutex_unlock(amp;timer->StateMutex);
return 0;
}
void
Timer_Initialize(Timer timer, void (*callback)(bool isCancelled, void * context))
{
pthread_mutex_init(amp;timer->ConditionMutex, NULL);
timer->IsRunning = false;
timer->Callback = callback;
pthread_mutex_init(amp;timer->StateMutex, NULL);
pthread_cond_init(amp;timer->Condition, NULL);
}
bool
Timer_IsRunning(Timer timer)
{
pthread_mutex_lock(amp;timer->StateMutex);
bool isRunning = timer->IsRunning;
pthread_mutex_unlock(amp;timer->StateMutex);
return isRunning;
}
void
Timer_Start(Timer timer, int timeoutMicroseconds, void * context)
{
timer->Context = context;
timer->TimeoutMicroseconds = timeoutMicroseconds;
pthread_create(amp;timer->Thread, NULL, TimerTask, (void *)timer);
}
void
Timer_Stop(Timer timer)
{
void * returnValue;
pthread_mutex_lock(amp;timer->StateMutex);
if (!timer->IsRunning)
{
pthread_mutex_unlock(amp;timer->StateMutex);
return;
}
pthread_mutex_unlock(amp;timer->StateMutex);
pthread_cond_broadcast(amp;timer->Condition);
pthread_join(timer->Thread, amp;returnValue);
}
void
Timer_WaitFor(Timer timer)
{
void * returnValue;
pthread_join(timer->Thread, amp;returnValue);
}
Пример использования:
void
TimerExpiredCallback(bool cancelled, void * context)
{
fprintf(stderr, "TimerExpiredCallback %s with context %sn",
cancelled ? "Cancelled" : "Timed Out",
(char *)context);
}
void
ThreadedTimerExpireTest()
{
TimerObject timerObject;
Timer_Initialize(amp;timerObject, TimerExpiredCallback);
Timer_Start(amp;timerObject, 5 * 1000000, "Threaded Timer Expire Test");
Timer_WaitFor(amp;timerObject);
}
void
ThreadedTimerCancelTest()
{
TimerObject timerObject;
Timer_Initialize(amp;timerObject, TimerExpiredCallback);
Timer_Start(amp;timerObject, 5 * 1000000, "Threaded Timer Cancel Test");
Timer_Stop(amp;timerObject);
}
Комментарии:
1. Не скрывайте природу указателя за typedef (как
Timer
это делает ваш). Это скорее сбивает с толку, чем помогает.2. Время чтения (7) . Рассмотрите также возможность использования некоторого цикла событий (например, libevent или Glib и т. Д.). Прочитайте руководство по pthread и системные вызовы (2) с помощью signal (7)
Ответ №1:
В целом, это кажется довольно надежной работой для тех, кто обычно работает на разных языках и у кого мало опыта работы с pthreads. Идея, похоже, вращается вокруг pthread_cond_timedwait()
достижения программируемой задержки с удобным механизмом отмены. Это не лишено смысла, но действительно есть несколько проблем.
Во-первых, использование переменной условия неидиоматично. Традиционное и идиоматическое использование переменной условия связывает с каждым ожиданием условие того, свободен ли поток для продолжения. Это проверяется под защитой мьютекса перед ожиданием. Если условие выполнено, то ожидание не выполняется. Он снова проверяется после каждого пробуждения, потому что существует множество сценариев, в которых поток может вернуться из ожидания, даже если на самом деле неясно, как продолжить. В этих случаях он возвращается и снова ожидает.
Я вижу как минимум две такие возможности с вашим таймером:
- Таймер отменяется очень быстро, прежде чем его поток начнет ждать. Условные переменные не ставят сигналы в очередь, поэтому в этом случае отмена будет неэффективной. Это форма состояния гонки.
- Ложное пробуждение. Это всегда возможность, которую необходимо учитывать. Ложные пробуждения редки в большинстве случаев, но они действительно случаются.
Мне кажется естественным решить эту проблему, обобщив ваш IsRunning
, чтобы охватить больше состояний, возможно, что-то более похожее
enum { NEW, RUNNING, STOPPING, FINISHED, ERROR } State;
вместо этого.
Конечно, вам все равно нужно протестировать это под защитой соответствующего мьютекса, что подводит меня к следующему пункту: одного мьютекса должно быть достаточно. Он может и должен служить как для защиты общего состояния, так и в качестве мьютекса, связанного с ожиданием CV. Это тоже идиоматично. Это привело бы к TimerTask()
более похожему коду:
// ...
pthread_mutex_lock(amp;timer->StateMutex);
// Responsibility for setting the state to RUNNING transferred to Timer_Start()
while (timer->State == RUNNING) {
returnValue = pthread_cond_timedwait(amp;timer->Condition, amp;timer->StateMutex, amp;timespec);
switch (returnValue) {
case 0:
if (timer->State == STOPPING) {
timer->State = FINISHED;
}
break;
case ETIMEDOUT:
timer->State = FINISHED;
break;
default:
timer->State = ERROR;
break;
}
}
pthread_mutex_unlock(amp;timer->StateMutex);
// ...
Сопровождающий Timer_Start()
и Timer_Stop()
будет примерно таким:
void Timer_Start(Timer timer, int timeoutMicroseconds, void * context) {
timer->Context = context;
timer->TimeoutMicroseconds = timeoutMicroseconds;
pthread_mutex_lock(amp;timer->StateMutex);
timer->state = RUNNING;
// start the thread before releasing the mutex so that no one can see state
// RUNNING before the thread is actually running
pthread_create(amp;timer->Thread, NULL, TimerTask, (void *)timer);
pthread_mutex_unlock(amp;timer->StateMutex);
}
void Timer_Stop(Timer timer) {
_Bool should_join = 0;
pthread_mutex_lock(amp;timer->StateMutex);
switch (timer->State) {
case NEW:
timer->state = FINISHED;
break;
case RUNNING:
timer->state = STOPPING;
should_join = 1;
break;
case STOPPING:
should_join = 1;
break;
// else no action
}
pthread_mutex_unlock(amp;timer->StateMutex);
// Harmless if the timer has already stopped:
pthread_cond_broadcast(amp;timer->Condition);
if (should_join) {
pthread_join(timer->Thread, NULL);
}
}
В другом месте потребуется несколько других, меньших настроек.
Кроме того, хотя в приведенном выше примере кода это опущено для наглядности, вам действительно следует убедиться, что вы проверяете возвращаемые значения всех функций, которые предоставляют информацию о состоянии таким образом, если вас не волнует, удалось ли им. Это включает в себя почти все стандартные функции библиотеки и Pthreads. То, что вы должны делать в случае сбоя, зависит от контекста, но притворяться (или предполагать), что это удалось, редко бывает хорошим выбором.
Альтернативный
Другой подход к отменяемой задержке будет вращаться вокруг select()
или pselect()
с таймаутом. Чтобы организовать отмену, вы настраиваете канал и должны select()
прослушать конец чтения. Запись чего-либо в конец записи приведет к пробуждению select()
.
Это несколькими способами проще кодировать, потому что вам не нужны никакие мьютексы или переменные условия. Кроме того, данные, записанные в канал, сохраняются до тех пор, пока они не будут прочитаны (или канал закрыт), что сглаживает некоторые проблемы, связанные с синхронизацией, которые приходится решать при использовании подхода, основанного на CV.
С select
, однако, вы должны быть готовы иметь дело с сигналами (как минимум, блокируя их), а время ожидания — это продолжительность, а не абсолютное время.
Ответ №2:
pthread_mutex_lock(amp;timer->StateMutex);
timer->IsRunning = true;
pthread_mutex_unlock(amp;timer->StateMutex);
pthread_mutex_lock(amp;timer->ConditionMutex);
returnValue = pthread_cond_timedwait(amp;timer->Condition, amp;timer->ConditionMutex, amp;timespec);
pthread_mutex_unlock(amp;timer->ConditionMutex);
if (timer->Callback != NULL)
{
(*timer->Callback)(returnValue != ETIMEDOUT, timer->Context);
}
У вас здесь есть две ошибки.
- Отмена может произойти после
IsRunning
того, как будет установлено значениеtrue
и доpthread_cond_timedwait
того, как будет вызван вызов. В этом случае вы будете ждать весь таймер. Эта ошибка существует, потомуConditionMutex
что не защищает какое-либо общее состояние. Чтобы правильно использовать переменную условия, мьютекс, связанный с переменной условия, должен защищать общее состояние. Вы не можете обменять правильный мьютекс на неправильный мьютекс, а затем вызватьpthread_cond_timedwait
, потому что это создает условие гонки. Весь смысл переменной условия состоит в том, чтобы обеспечить атомарную операцию «разблокировки и ожидания», чтобы предотвратить это состояние гонки, и ваш код прилагает усилия, чтобы нарушить эту логику. - Вы не проверяете возвращаемое значение
pthread_cond_timedwait
. Если ни тайм-аут не истек, ни отмена не была запрошена, вы все равно вызываете обратный вызов. Переменные условия не имеют состояния. Вы несете ответственность за отслеживание и проверку состояния, переменная условия не сделает этого за вас. Вам нужно вызыватьpthread_cond_timedwait
цикл до тех пор, пока неstate
будет установлено значениеSTOPPING
или не будет достигнут тайм-аут. Обратите внимание, что мьютекс, связанный с переменной условия, как в 1 выше, должен защищать общее состояние — в данном случаеstate
.
Я думаю, у вас фундаментальное непонимание того, как работают переменные условия и для чего они предназначены. Они используются, когда у вас есть мьютекс, который защищает общее состояние, и вы хотите дождаться изменения этого общего состояния. Мьютекс, связанный с переменной условия, должен защищать общее состояние, чтобы избежать классического состояния гонки, когда состояние изменяется после того, как вы сняли блокировку, но до того, как вам удалось начать ожидание.
Обновить:
Чтобы предоставить еще немного полезной информации, позвольте мне кратко объяснить, для чего предназначена условная переменная. Допустим, у вас есть какое-то общее состояние, защищенное мьютексом. И скажем, какой-то поток не может продвигаться вперед, пока это общее состояние не изменится.
У вас проблема. Вы должны удерживать мьютекс, который защищает общее состояние, чтобы увидеть, что это за состояние. Когда вы видите, что он находится в неправильном состоянии, вам нужно подождать. Но вам также необходимо освободить мьютекс, иначе никакой другой поток не сможет изменить общее состояние.
Но если вы разблокируете мьютекс, а затем подождете (что и делает ваш код выше!) у вас состояние гонки. После того, как вы разблокируете мьютекс, но прежде чем ждать, другой поток может получить мьютекс и изменить общее состояние таким образом, чтобы вы больше не хотели ждать. Таким образом, вам нужна атомарная операция «разблокируйте мьютекс и подождите».
Это цель и единственная цель переменных условий. Таким образом, вы можете атомарно освободить мьютекс, который защищает некоторое общее состояние, и ждать знака без изменений, чтобы сигнал был потерян между тем, когда вы выпустили мьютекс, и когда вы ждали.
Еще один важный момент — переменные условия не имеют состояния. Они понятия не имеют, чего вы ждете. Вы никогда не должны звонить pthread_cond_wait
или pthread_cond_timedwait
делать предположения о состоянии. Вы должны проверить это самостоятельно. Ваш код освобождает мьютекс после pthread_cond_timedwait
возврата. Вы хотите сделать это только в том случае, если время ожидания вызова истекло.
Если pthread_cond_timedwait
не истекает время ожидания (или, в любом случае, когда pthread_cond_wait
возвращается), вы не знаете, что произошло, пока не проверите состояние. Вот почему эти функции повторно получают мьютекс — чтобы вы могли проверить состояние и решить, что делать. Вот почему эти функции почти всегда вызываются в цикле — если то, чего вы ждете, все еще не произошло (что вы определяете, проверяя общее состояние, за которое вы несете ответственность), вам нужно продолжать ждать.
Комментарии:
1. Привет, Дэвид, спасибо за ваши комментарии. Я уверен, что неправильно понимаю некоторые концепции, поскольку я впервые их использую, но документация и примеры, доступные мне в режиме онлайн, не очень полезны. Документация почти предполагает предварительные знания, что смешно, а примеры настолько сложны, что бесполезны. Как только я усвою концепции, я напишу руководство для идиотов, основанное на моем опыте.
2. @Tim Я предоставил некоторую дополнительную информацию, которая, я надеюсь, будет полезной.